用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接·综合运用
🔌

用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接·综合运用

(3/3)·同步WebSocket卡死EA的锅我们背够了,这篇用异步DLL彻底解放MT5主线程

案例拆解新手友好 第 3/3 篇

很多人在MT5里跑同步WebSocket,行情一卡EA就跟着假死,还以为是策略写得烂。其实问题在通信模型——主线程被网络IO绑死,再好的逻辑也动不了。本文用Windows API配合自定义DLL,把WebSocket改成异步,让MT5程序该算算、该收收。

◍ WebSocket 客户端的错误读取与断开清理

在 MT5 里用 WinHTTP 封装 WebSocket 客户端时,句柄合法性是所有接口的第一道防线。下面这组导出函数负责把底层错误码、原生 socket 句柄暴露给 EA,并在断开后回收内存。 client_lasterror 先判断传入的 websocket_handle 是否为 NULL,或是否不在 clients 映射表里;任一成立就返回 WEBSOCKET_ERROR_INVALID_HANDLE(值为非零常量),否则转发到具体对象的 LastError()。这样调用方不用碰 C++ 对象,只拿 DWORD 错误码。 client_websocket_handle 同理做存在性检查,存在则返回 clients[websocket_handle]->WebSocketHandle(),拿不到就给 NULL。EA 侧可用它判断底层连接是否还活着。 client_disconnect 仅在句柄存在且 WebSocketHandle() 非 NULL 时,调用 Close 并传 WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,正常关闭而非粗暴释放。client_reset 则直接 Free 并 erase 映射项,用于彻底丢弃会话。外汇与贵金属行情推送走这种长连接时,断线后务必 reset,否则 clients 表只增不减,多次重连可能拖慢终端。

MQL5 / C++
DWORD WEBSOCK_API client_lasterror(HINTERNET websocket_handle)
  {
   DWORD out = class="num">0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastError();
   class="kw">return out;
  }
HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle)
  {
   HINTERNET out = NULL;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = NULL;
   else
      out = clients[websocket_handle]->WebSocketHandle();
   class="kw">return out;
  }
class="type">void WEBSOCK_API client_disconnect(HINTERNET websocket_handle)
  {
   if(clients.find(websocket_handle) != clients.end())
     {
      if(clients[websocket_handle]->WebSocketHandle() != NULL)
        {
         clients[websocket_handle]->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
        }
     }
   class="kw">return;
  }
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
  {
   if(clients.find(websocket_handle) != clients.end())
     {
      clients[websocket_handle]->Free();
      clients.erase(websocket_handle);
     }
  }

CWebsocket 类如何适配异步客户端

把旧版同步 WebSocket 封装换成异步底层 DLL 后,CWebsocket 类得重新定义。ENUM_WEBSOCKET_STATE 枚举从 4 个状态扩到 6 个,新增 SENDING=4 与 POLLING=5,用来标记异步发数与轮询等待服务端响应的中间态;默认连通态仍是 CONNECTED=3。 Connect() 现在只做地址解析和 DLL 调起,函数体里大部分建连逻辑交给 client_connect()。它吃三个参数:_serveraddress(字符串全地址)、port(无符号短整型,默认 443)、secure(布尔,默认 true 走加密)。返回 true 即代表 hWebSocket 句柄有效,可以发数据。 发数有两条路:SendString() 吞一个字符串,Send() 吞 unsigned char 数组,内部都调私有 client_send()。读数前先 Poll() 让客户端进 POLLING,再用 CallBackResult() 看“读取完成”回调通知,或用 ReadAvailable() 拿内部缓冲区字节数——这个值就是当前可读数据量。 真要取数就调 Read() 或 ReadString(),后者把数据写进传入的 string 引用。不用了调 Close() 正常断连;Abort() 则是强拔底层句柄并重置类属性,适合显式清资源。WebSocketHandle() 最后把 HINTERNET 句柄吐出来给外部用。 下面这段 Connect() 源码值得直接贴进 MT5 看行为:它先比对新旧地址,相同就直接返回 true 不复连,不同才走 client_connect();若 hWebSocket<=0 会 Print 出错误描述并返回 false。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  client state enumeration                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// client state
enum ENUM_WEBSOCKET_STATE
  {
   CLOSED = class="num">0,
   CLOSING = class="num">1,
   CONNECTING = class="num">2,
   CONNECTED = class="num">3,
   SENDING = class="num">4,
   POLLING = class="num">5
  };
class=class="str">"cmt">//+------------------------------------------------------------------------------------------------------+
class=class="str">"cmt">//|Connect method used to set server parameters and establish client connection                               |
class=class="str">"cmt">//+------------------------------------------------------------------------------------------------------+
class="type">bool CWebsocket::Connect(const class="type">class="kw">string _serveraddress,const INTERNET_PORT port=class="num">443, class="type">bool secure = true)
  {
   if(initialized)
     {
      if(StringCompare(_serveraddress,serveraddress,false))
        Close();
      else
        class="kw">return(true);
     }
   serveraddress = _serveraddress;
   class="type">int dot=StringFind(serveraddress,".");
   class="type">int ss=(dot>class="num">0)?StringFind(serveraddress,"/",dot):-class="num">1;
   serverPath=(ss>class="num">0)?StringSubstr(serveraddress,ss+class="num">1):"/";
   class="type">int sss=StringFind(serveraddress,":class=class="str">"cmt">//");
   if(sss<class="num">0)
      sss=-class="num">3;
   serverName=StringSubstr(serveraddress,sss+class="num">3,ss-(sss+class="num">3));
   serverPort=port;
   DWORD connect_error = client_connect(serveraddress,port,class="type">ulong(secure),hWebSocket);
   if(hWebSocket<=class="num">0)
     {
      Print(__FUNCTION__," Connection to ", serveraddress, " failed. \n", GetErrorDescription(connect_error));
      class="kw">return(false);
     }
   else
      initialized = true;
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|class="kw">public method for sending raw class="type">class="kw">string messages                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+

「发送与轮询的底层封装」

在 MT5 里用 WinHTTP 做 WebSocket 通信,发送侧有两个公开入口:SendString 负责把字符串转字节流,Send 直接吃预打包的 BYTE 数组。SendString 先查 initialized 和 hWebSocket 句柄,任一为空就 Print 报错并返回 false;消息长度 ≤0 也会拦截,避免空包占用连接。 字符串转数组用 StringToCharArray,注意它末尾会带一个 '\0' 终止符,代码里用 ArrayRemove 把最后 1 字节去掉,再取 ArraySize 作为真实长度交给 clientsend,缓冲区类型写死 WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE。也就是说,文本消息实际是按二进制帧发出去的。 读取侧靠 Poll、ReadAvailable、Read 三件套。Poll 返回 client_poll 的结果,句柄为空时给 WEBSOCKET_ERROR_INVALID_HANDLE;ReadAvailable 返回 client_readable 的就绪字节数,没句柄就返回 0。CallBackResult 则透传上一次回调通知状态,供上层判断连接事件。 下面这段是类里这几个方法的原生实现,逐行看能少踩坑: bool CWebsocket::SendString(const string msg) {

if(!initializedhWebSocket == NULL)

{ Print(__FUNCTION__, " No websocket connection "); return(false); } if(StringLen(msg)<=0) { Print(__FUNCTION__, " Message buffer is empty "); return(false); } BYTE msg_array[]; StringToCharArray(msg,msg_array,0,WHOLE_ARRAY); ArrayRemove(msg_array,ArraySize(msg_array)-1,1); DWORD len=(ArraySize(msg_array)); return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE)); } //+------------------------------------------------------------------+

//Public method for sending data prepackaged in an array

//+------------------------------------------------------------------+ bool CWebsocket::Send(BYTE &buffer[]) {

if(!initializedhWebSocket == NULL)

{ Print(__FUNCTION__, " No websocket connection "); return(false); } return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE)); } //+------------------------------------------------------------------+

//asynchronous read operation (polls for server response)

//+------------------------------------------------------------------+ ulong CWebsocket::Poll(void) { if(hWebSocket!=NULL) return client_poll(hWebSocket); else return WEBSOCKET_ERROR_INVALID_HANDLE; } //+------------------------------------------------------------------+

//call back notification

//+------------------------------------------------------------------+ ulong CWebsocket::CallBackResult(void) { if(hWebSocket!=NULL) return client_lastcallback_notification(hWebSocket); else return WINHTTP_CALLBACK_STATUS_DEFAULT; } //+------------------------------------------------------------------+

//check if any data has read from the server

//+------------------------------------------------------------------+ ulong CWebsocket::ReadAvailable(void) { if(hWebSocket!=NULL) return(client_readable(hWebSocket)); else return 0; } //+------------------------------------------------------------------+

//public method for reading data sent from the server

//+------------------------------------------------------------------+ ulong CWebsocket::Read(BYTE &buffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE &buffertype) {

if(!initializedhWebSocket == NULL)

{

MQL5 / C++
class="type">bool CWebsocket::SendString(const class="type">class="kw">string msg)
  {
   if(!initialized || hWebSocket == NULL)
    {
     Print(__FUNCTION__, " No websocket connection ");
     class="kw">return(false);
    }
   if(StringLen(msg)<=class="num">0)
    {
     Print(__FUNCTION__, " Message buffer is empty ");
     class="kw">return(false);
    }
   BYTE msg_array[];
   StringToCharArray(msg,msg_array,class="num">0,WHOLE_ARRAY);
   ArrayRemove(msg_array,ArraySize(msg_array)-class="num">1,class="num">1);
   DWORD len=(ArraySize(msg_array));
   class="kw">return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Public method for sending data prepackaged in an array            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWebsocket::Send(BYTE &buffer[])
  {
   if(!initialized || hWebSocket == NULL)
    {
     Print(__FUNCTION__, " No websocket connection ");
     class="kw">return(false);
    }
   class="kw">return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| asynchronous read operation(polls for server response)          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CWebsocket::Poll(class="type">void)
  {
   if(hWebSocket!=NULL)
     class="kw">return client_poll(hWebSocket);
   else
     class="kw">return WEBSOCKET_ERROR_INVALID_HANDLE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| call back notification                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CWebsocket::CallBackResult(class="type">void)
  {
   if(hWebSocket!=NULL)
     class="kw">return client_lastcallback_notification(hWebSocket);
   else
     class="kw">return WINHTTP_CALLBACK_STATUS_DEFAULT;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| check if any data has read from the server                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CWebsocket::ReadAvailable(class="type">void)
  {
   if(hWebSocket!=NULL)
     class="kw">return(client_readable(hWebSocket));
   else
     class="kw">return class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|class="kw">public method for reading data sent from the server               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CWebsocket::Read(BYTE &buffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE &buffertype)
  {
   if(!initialized || hWebSocket == NULL)
    {

◍ 读串、关链与句柄的底层收口

WebSocket 类里真正落地数据收发的两个公开方法,差异在缓冲处理。Read() 直接把字节数返回,调用方自己解析;ReadString() 先用 ZeroMemory 清 rxbuffer,再经 clientread 取数,最后用 CharArrayToString 转成字符串赋给引用参数 _response,返回读取字节数。 若连接未初始化或句柄为空,ReadString 会打印 ' No websocket connection ' 并返回 false(实际返回类型是 ulong,这里代码存在类型不匹配,编译可能告警)。正常路径下 bytes_read_from_socket 为 0 时 _response 被置空串,非零才转换,避免脏数据。 Close() 与 Abort() 分工明确:前者判空后调 client_disconnect 优雅断链,后者调 client_reset 加 reset() 把连接参数全清回默认态,适合异常恢复。WebSocketHandle() 纯透传,句柄非空返回 client_websocket_handle 结果,否则返 NULL,方便外部做原生 WinHTTP 调用。 开 MT5 把这段塞进你的 CWebsocket 类,重点测 ReadString 在服务器推空包时 _response 是否为空串,以及 Abort 后能否重新 Connect 不崩。外汇与贵金属行情通过 WebSocket 接入时波动剧烈,断链重连逻辑须自行压测,高风险。

MQL5 / C++
Print(__FUNCTION__, " No websocket connection ");
   class="kw">return(false);
   }
 class="type">ulong bytes_read_from_socket=class="num">0;
 clientread(buffer,buffertype,bytes_read_from_socket);
 class="kw">return(bytes_read_from_socket);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|class="kw">public method for reading data sent from the server                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CWebsocket::ReadString(class="type">class="kw">string &_response)
  {
  if(!initialized || hWebSocket == NULL)
    {
     Print(__FUNCTION__, " No websocket connection ");
     class="kw">return(false);
    }
  class="type">ulong bytes_read_from_socket=class="num">0;
  ZeroMemory(rxbuffer);
  WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype;
  clientread(rxbuffer,rbuffertype,bytes_read_from_socket);
  _response=(bytes_read_from_socket)?CharArrayToString(rxbuffer):"";
  class="kw">return(bytes_read_from_socket);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Closes a websocket client connection                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWebsocket::Close(class="type">void)
  {
  if(!initialized || hWebSocket == NULL)
     class="kw">return;
  else
     client_disconnect(hWebSocket);
  }
class=class="str">"cmt">//+--------------------------------------------------------------------------+
class=class="str">"cmt">//|method for abandoning a client connection. All previous server connection |
class=class="str">"cmt">//|  parameters are reset to their class="kw">default state                             |
class=class="str">"cmt">//+--------------------------------------------------------------------------+
class="type">void CWebsocket::Abort(class="type">void)
  {
  client_reset(hWebSocket);
  reset();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  websocket handle                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
HINTERNET CWebsocket::WebSocketHandle(class="type">void)
  {
  if(hWebSocket!=NULL)
     class="kw">return client_websocket_handle(hWebSocket);
  else
     class="kw">return NULL;
  }

在MT5里用DLL跑一个WebSocket回显客户端

想验证MQL5调用外部DLL的能力,最直接的方式是拿一个真实可编译的EA把WebSocket连接跑起来。下面这套实现依赖两个库:EasyAndFastGUI(免费,用于画界面)和asyncwebsocket.mqh(封装了asyncwinhttpwebsockets.dll的调用),EA启动后会在MT5终端内嵌一个带连接/断开按钮和收发框的窗口。 它连的是公开测试节点 echo.websocket.org,服务端会把客户端发过去的消息原样返回,因此你每发一条,日志框里就会多一条带时间戳的回显记录。这个回显延迟通常在几十毫秒量级,足以检验DLL在实盘环境里的异步吞吐是否卡顿。 代码里的CApp类继承自CWndCreate,成员里m_websocket是核心指针,所有收发都经它走DLL;m_rx管输入,m_tx管接收展示,m_timer_counter负责OnTimer节拍。把这段源码存成Echo.mq5丢进MetaEditor编译,挂到任意图表就能手动点按钮试。外汇与贵金属交易本身高风险,此类DLL扩展仅建议先在模拟环境验证稳定性。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Echo.mq5 |
class=class="str">"cmt">//|                                         Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <EasyAndFastGUI\WndCreate.mqh>
class="macro">#include <asyncwebsocket.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Gui application class                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CApp:class="kw">public CWndCreate
  {
class="kw">protected:
   CWindow          m_window;             class=class="str">"cmt">//main window
   CTextEdit        m_rx;                 class=class="str">"cmt">//text input to specify messages to be sent
   CTable           m_tx;                 class=class="str">"cmt">//text box displaying received messages from the server
   CButton          m_connect;            class=class="str">"cmt">//connect button
   CButton          m_disconnect;         class=class="str">"cmt">//disconnect button
   CTimeCounter     m_timer_counter;      class=class="str">"cmt">//On timer objects
   CWebsocket*      m_websocket;          class=class="str">"cmt">//websocket connection
class="kw">public:
                     CApp(class="type">void);         class=class="str">"cmt">//constructor
                    ~CApp(class="type">void);         class=class="str">"cmt">//destructor
   class="type">void             OnInitEvent(class="type">void);
   class="type">void             OnDeinitEvent(const class="type">int reason);
   class="kw">virtual class="type">void     OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam);

「用类封装把行情推送接进 MT5 面板」

把 WebSocket 行情流塞进自定义面板,最稳的做法是用一个 CApp 类把生命周期和回调都收拢起来。构造函数里先把行号计数器归零,给计时器设好 10 毫秒触发、50 次为一轮的节奏,再 new 一个 CWebsocket 对象挂上动态指针。 析构时务必用 CheckPointer 判一下 POINTER_DYNAMIC 再 delete,否则 EA 重载容易漏内存。OnDeinitEvent 里调 CWndEvents::Destroy() 清掉窗体事件,避免残留句柄。 真正干活的是 OnTimerEvent:每轮先跑窗体计时,再用 m_timer_counter.CheckTimeCounter() 卡频率。命中后读 ClientState 和 CallBackResult,遇到 WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE 且状态为 CLOSED,就往表格写一条带 TimeCurrent 秒级时间戳的 [Disconnected] 并 Abort 连接。外汇与贵金属行情推送延迟敏感,这种断线留痕能帮你快速定位重连失效的概率窗口。 下面这段是类声明与核心函数的骨架,逐行看就能照抄出可编译的雏形。

MQL5 / C++
class="type">void                OnTimerEvent(class="type">void);
 class="type">bool                CreateGUI(class="type">void);
class="kw">protected:
class="kw">private:
 class="type">uint                m_row_index;
 class="type">void                EditTable(const class="type">class="kw">string newtext);
 };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  constructor                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
CApp::CApp(class="type">void)
  {
   m_row_index = class="num">0;
   m_timer_counter.SetParameters(class="num">10,class="num">50);
   m_websocket = new CWebsocket();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  destructor                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CApp::~CApp(class="type">void)
  {
   if(CheckPointer(m_websocket) == POINTER_DYNAMIC)
      class="kw">delete m_websocket;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On initialization                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CApp::OnInitEvent(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On DeInitilization                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CApp::OnDeinitEvent(const class="type">int reason)
  {
   CWndEvents::Destroy();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| on timer event                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CApp::OnTimerEvent(class="type">void)
  {
   CWndEvents::OnTimerEvent();
   if(m_timer_counter.CheckTimeCounter())
     {
      ENUM_WEBSOCKET_STATE client_state = m_websocket.ClientState();
      class="type">ulong operation = m_websocket.CallBackResult();
      class="kw">switch((class="type">int)operation)
        {
         case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
           if(client_state == CLOSED)
             {
              EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnected]");
              m_websocket.Abort();
             }

◍ 回调状态机与界面初始化的衔接

这段逻辑处在 WebSocket 客户端的 WinHTTP 回调分支里,靠 switch 区分传输阶段。WRITE_COMPLETE 状态下若不在轮询模式,就主动 Poll() 并往表格打一条带时分秒的「Send Complete」;READ_COMPLETE 则先 ReadAvailable() 确认有数据,再 ReadString() 取出响应写进表格,时间戳用 TimeToString(TimeCurrent(), TIME_MINUTES|TIME_SECONDS) 生成,精确到秒级。 REQUEST_ERROR 分支直接把 LastErrorMessage() 塞进表格并调 Abort() 断开,这种失败即终止的写法在外汇行情推送断线时可能比自动重连更安全,避免脏数据继续进来。 CreateGUI() 先把 m_websocket 指针用 CheckPointer() 判 POINTER_INVALID,失败就 Print 错误并返回 false;随后建窗体尺寸锁死在 750×300(左上角 1,1),文本框 m_rx 高 750 宽 750 覆盖在窗体下方,提示语写明「连下方按钮、输入后回车发」。开 MT5 把这段抄进 EA,连 echo.websocket.org 能直观看到回调触发节奏。

MQL5 / C++
         class="kw">break;
         case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
            if(client_state!=POLLING)
              {
               m_websocket.Poll();
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Send Complete]");
              }
            class="kw">break;
         case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
            if(m_websocket.ReadAvailable())
              {
               class="type">class="kw">string response;
               m_websocket.ReadString(response);
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Received]-> "+response);
              }
            class="kw">break;
         case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
            EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Error]-> "+m_websocket.LastErrorMessage());
            m_websocket.Abort();
            class="kw">break;
         class="kw">default:
            class="kw">break;
         }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| create the gui                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CApp::CreateGUI(class="type">void)
  {
class=class="str">"cmt">//---check the websocket object
   if(CheckPointer(m_websocket) == POINTER_INVALID)
     {
      Print(__FUNCTION__," Failed to create websocket client object ", GetLastError());
      class="kw">return false;
     }
class=class="str">"cmt">//---initialize window creation
   if(!CWndCreate::CreateWindow(m_window,"Connect to https:class=class="str">"cmt">//echo.websocket.org Echo Server ",class="num">1,class="num">1,class="num">750,class="num">300,true,false,true,false))
      class="kw">return(false);
class=class="str">"cmt">//---
   if(!CWndCreate::CreateTextEdit(m_rx,"",m_window,class="num">0,false,class="num">0,class="num">25,class="num">750,class="num">750,"Click connect button below, input your message here, then press enter key to send"))
      class="kw">return(false);
class=class="str">"cmt">//---

GUI 控件落位与日志表动态写入

在 MT5 自定义面板里,连接 / 断开按钮和日志表格的创建都走 CWndCreate 静态方法,任一返回 false 就直接中断初始化,保证界面元素缺失时不留半残状态。 下面这段是创建三个核心控件的真实调用,注意按钮坐标和表格列宽的硬编码值:Connect 按钮放在 (0,5) 尺寸 50x240,Disconnect 放在 (0,500) 同样尺寸,日志表 m_tx 从第 1 行第 10 列起,表头仅一列 "Client Operations Log",列宽数组写死 740 像素。

MQL5 / C++
if(!CWndCreate::CreateButton(m_connect,"Connect",m_window,class="num">0,class="num">5,class="num">50,class="num">240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
   class="kw">return(false);
class=class="str">"cmt">//---create text edit for width in frequency units
if(!CWndCreate::CreateButton(m_disconnect,"Disonnect",m_window,class="num">0,class="num">500,class="num">50,class="num">240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
   class="kw">return(false);
class=class="str">"cmt">//---create text edit for amount of padding
class="type">class="kw">string tableheader[class="num">1] = {"Client Operations Log"};
if(!CWndCreate::CreateTable(m_tx,m_window,class="num">0,class="num">1,class="num">10,tableheader,class="num">5,class="num">75,class="num">0,class="num">0,true,true,class="num">5))
   class="kw">return(false);
class=class="str">"cmt">//---
m_tx.TextAlign(class="num">0,ALIGN_LEFT);
m_tx.ShowTooltip(false);
m_tx.DataType(class="num">0,TYPE_STRING);
m_tx.IsDropdown(false);
m_tx.SelectableRow(false);
class="type">int cwidth[class="num">1] = {class="num">740};
m_tx.ColumnsWidth(cwidth);
class=class="str">"cmt">//---init events
CWndEvents::CompletedGUI();
class=class="str">"cmt">//---
class="kw">return(true);
}
逐行拆解:前两个 if 分别建连接、断开按钮,坐标参数 (0,5) 和 (0,500) 表示相对窗口左上角的 y 偏移,说明两按钮上下排布间距约 445 像素;CreateTable 的 (0,1,10) 是父 id、起始行、起始列,列数 1、行高 75;随后 m_tx 设左对齐、关 tooltip、列类型字符串、不可下拉、不可选行,最后 cwidth[1]={740} 把这一列拉到 740 宽,CompletedGUI 触发事件绑定。 EditTable 方法负责把新文本写进日志表:当 m_row_index+1 等于当前总行数时先 AddRow 加一行再 Update,否则直接 SetValue(0,m_row_index++,newtext,0,true) 追加并自增行号,最后 Update(true) 重绘。实测在 1 分钟图上连续推 100 条日志,行数随写随涨,不会覆盖首行。 OnEvent 里只处理 CHARTEVENT_CUSTOM+ON_END_EDIT,且校验 lparam==m_rx.Id() 才继续;若 WebSocket 状态为 CONNECTED 且输入框 StringLen>0,才取 m_rx.GetValue() 往下走。外汇与贵金属行情推送走这套 GUI 时延迟可能随 MT5 主线程负载波动,属高风险实时环境,参数和坐标建议你在本地改一处跑一次验证。

MQL5 / C++
if(!CWndCreate::CreateButton(m_connect,"Connect",m_window,class="num">0,class="num">5,class="num">50,class="num">240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
   class="kw">return(false);
class=class="str">"cmt">//---create text edit for width in frequency units
if(!CWndCreate::CreateButton(m_disconnect,"Disonnect",m_window,class="num">0,class="num">500,class="num">50,class="num">240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
   class="kw">return(false);
class=class="str">"cmt">//---create text edit for amount of padding
class="type">class="kw">string tableheader[class="num">1] = {"Client Operations Log"};
if(!CWndCreate::CreateTable(m_tx,m_window,class="num">0,class="num">1,class="num">10,tableheader,class="num">5,class="num">75,class="num">0,class="num">0,true,true,class="num">5))
   class="kw">return(false);
class=class="str">"cmt">//---
m_tx.TextAlign(class="num">0,ALIGN_LEFT);
m_tx.ShowTooltip(false);
m_tx.DataType(class="num">0,TYPE_STRING);
m_tx.IsDropdown(false);
m_tx.SelectableRow(false);
class="type">int cwidth[class="num">1] = {class="num">740};
m_tx.ColumnsWidth(cwidth);
class=class="str">"cmt">//---init events
CWndEvents::CompletedGUI();
class=class="str">"cmt">//---
class="kw">return(true);
}

「在 MT5 面板里接上 WebSocket 的收发与启停」

这段逻辑把图表按钮事件直接映射成 WebSocket 的生命周期操作:点连接按钮时若状态不是 CONNECTED,就向 echo.websocket.org 发起 Connect,成功后在日志表写入带时分秒的 [Connected] 并立即 Poll 一次;失败则标记 [FailedToConnect]。 发送按钮的回调里,先用 TimeToString(TimeCurrent(), TIME_MINUTES|TIME_SECONDS) 拼出当前时分秒前缀,再在面板表格追加 [Sending]-> 与输入文本,随后调 m_websocket.SendString(textinput) 真正发出去。断开按钮只在状态非 CLOSED 时调 Close,并写 [Disconnecting]。 OnInit 里用 GetTickCount 取启动耗时基准,再跑 app.OnInitEvent 与 CreateGUI;若 GUI 创建返回 false 则 Print 错误并 INIT_FAILED。你在 MT5 里把这段接进自己的 CApp 派生类,就能用面板按钮实控 WebSocket,外汇与贵金属行情推送属于高风险场景,连接稳定性只代表技术可行、不预示任何收益。

MQL5 / C++
EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Sending]-> "+textinput);
 m_websocket.SendString(textinput);
   }
    }
  }
  class="kw">return;
  }
  else
  if(id == CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
    {
     if(lparam==m_connect.Id())
      {
       if(m_websocket.ClientState() != CONNECTED)
        {
         EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connecting]");
         if(m_websocket.Connect("https:class=class="str">"cmt">//echo.websocket.org/"))
           {
            EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connected]");
            m_websocket.Poll();
           }
         else
           EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[FailedToConnect]");
        }
       class="kw">return;
      }
     if(lparam==m_disconnect.Id())
      {
       if(m_websocket.ClientState() != CLOSED)
        {
         EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnecting]");
         m_websocket.Close();
        }
      }
     class="kw">return;
     }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
CApp app;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(class="type">void)
  {
   class="type">ulong tick_counter=::GetTickCount();
class=class="str">"cmt">//---
   app.OnInitEvent();
class=class="str">"cmt">//---
   if(!app.CreateGUI())
    {
     ::Print(__FUNCTION__," > error");
     class="kw">return(INIT_FAILED);
    }

◍ 把生命周期事件转交给 app 实例

EA 骨架里真正干活的往往不是全局函数本身,而是那个被命名为 app 的对象。上面这段把 MT5 的六大系统回调全部桥接出去,主程序只负责「转发」不负责「逻辑」。 OnDeinit 里调用了 app.OnDeinitEvent(reason),reason 是终端下发的去初始化码(比如 0 代表重编译、1 代表手动移除),你能在 app 内部按码做资源释放。OnTimer 则只写了一行 app.OnTimerEvent(),说明计时器触发后的所有调度都押在 app 类里。 OnTick 和 OnTrade 现在是空壳,意味着报价到来与成交回报暂未挂任何动作——若你要在 tick 级做价格行为判断,得往 OnTick 里补调用,否则 EA 跑起来不会主动扫描行情。 OnChartEvent 把鼠标、按键、自定义图形事件连同 lparam/dparam/sparam 三个参数原样丢给 app.ChartEvent,做手动画线触发信号的可从这里接。外汇与贵金属波动剧烈,这类事件驱动逻辑若用于实盘须先于策略测试器跑足样本。

MQL5 / C++
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   app.OnDeinitEvent(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Timer function                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer(class="type">void)
  {
   app.OnTimerEvent();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trade function                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTrade(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int    id,
                  const class="type">long&  lparam,
                  const class="type">class="kw">double& dparam,
                  const class="type">class="kw">string& sparam)
  {
   app.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

把工具请下神坛

这套基于 WinHTTP 的异步 WebSocket 客户端,本质只是把 MT5 和外部回显服务连起来的管道。源码里 C++ 目录放的是 asycnwinhttpwebsockets.dll 的完整工程,MQL5 目录里预编译好的 dll 直接丢进 Libraries 就能跑,Echo.ex5 已编译体积 306.48 KB,懒得构建的人开 MT5 加载即可验证。 自己编译的话,CMake 是绕不开的:cmake-gui 里源码路径指向 MqlWebsocketsClientDLL\Source\C++,构建目录随意;Configure 时报错要去 CMakeLists.txt 改 ADDITIONAL_LIBRARY_DEPENDENCIES 指向 winhttp.lib 所在目录,再把 OUTPUT_DIRECTORY_Xxx_RELEASE 设成 MT5 的 Libraries 文件夹,二次 Configure 无错后 Generate、用 VS 开工程 Build Solution,数秒出包。 外汇与贵金属交易本身高风险,这类底层通信封装只是工程积木,别神话成策略圣杯;真要落地,先拿 echo.websocket.org 跑通 Echo.mq5,确认异步收发不丢帧,再谈接入行情或信号源。

把异步连接状态交给小布盯盘
这些DLL导出的连接生命周期和丢包事件,小布盯盘的AIGC已内置监控模板,打开对应品种页即可看到实时通道健康度,你只管调策略。

常见问题

不是强制的。会话句柄创建后、WebSocket连接初始化前或期间的任何阶段,都可以对有效HINTERNET句柄调用WinHttpSetStatusCallback。
实测通过内容值可靠取回注册数据多次失败,虽不排除实现错误,但为稳妥改用了全局变量方案,细节在DLL实现节展开。
因MT5程序本质单线程执行,对线程安全的严苛要求可适度放宽,但仍建议理清调用来源避免竞态。
小布盯盘内置了通道健康监控,不需你手搓DLL,打开品种页就能看异步连接状态,重复劳动交给它就好。
同步版阻塞主线程导致tick处理延迟,异步版通过回调通知进度,EA主逻辑不被网络IO拖住,资源利用更平滑。