MetaTrader 5 中的 WebSockets·进阶篇
🔌

MetaTrader 5 中的 WebSockets·进阶篇

(2/3)· 轮询烧带宽、长连接空耗成本,MQL5 原生集成 WebSocket 才叫省心

实战向 第 2/3 篇
很多交易者以为 MQL5 只能靠 DLL 才能连外部系统,结果把平台安全边界和稳定性一并拖下水。其实原生 WebSocket 客户端不碰动态链接库也能跑通,只是多数人卡在协议认知和类封装上。用错旧式轮询,行情延迟和带宽浪费会悄悄吃掉你的边际收益。

「MT5 套接字类的证书结构与私有成员」

在 MT5 里做外部数据接入,CSocket 是基础封装类。它先定义了一个 CERT 结构,专门装服务器证书信息:所有者名称、颁发者、序列号、SHA-1 指纹、到期时间各占一个字段。 证书结构之外,类内部用了一组私有变量跟踪连接状态。m_usedsockets 是静态成员,记录单程序内当前活跃套接字总数;m_socket 存句柄,m_address 和 m_port 对应远端地址与端口;m_usetls 标记是否走 TLS,m_rx_timeout / m_tx_timeout 是收发超时(毫秒)。 对外暴露的读取接口很直接:SocketID 拿句柄,Address 和 Port 回传连接目标,IsSecure 看 TLS 开关,RxTimeout / TxTimeout 回超时值,ServerCertificate 抛回整张证书,SocketsInUse 直接读那个静态计数。 写超时和日志开关靠 SetTimeouts 与 Log 控制。真要连服务器,得先 Open 初始化并把日志输出打开,否则排错时你大概率看不到任何痕迹。外汇与贵金属行情走 socket 推送时延迟敏感,建议把超时设到 1000–3000 毫秒区间再压测。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| structs                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct CERT
  {
   class="type">class="kw">string            cert_subject;
   class="type">class="kw">string            cert_issuer;
   class="type">class="kw">string            cert_serial;
   class="type">class="kw">string            cert_thumbprint;
   class="type">class="kw">datetime          cert_expiry;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CSocket.                                                      |
class=class="str">"cmt">//| Purpose: Base class of socket operations.                           |
class=class="str">"cmt">//|                                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSocket
  {
class="kw">private:
   class="kw">static class="type">int        m_usedsockets;    class=class="str">"cmt">// tracks number of sockets in use in single program
   class="type">bool              m_log;            class=class="str">"cmt">// logging state
   class="type">bool              m_usetls;         class=class="str">"cmt">//  tls state
   class="type">uint              m_tx_timeout;     class=class="str">"cmt">//  send system socket timeout in milliseconds
   class="type">uint              m_rx_timeout;     class=class="str">"cmt">//  receive system socket timeout in milliseconds
   class="type">int               m_socket;         class=class="str">"cmt">//  socket handle
   class="type">class="kw">string            m_address;        class=class="str">"cmt">//  server address
   class="type">uint              m_port;           class=class="str">"cmt">//  port
   CERT              m_cert;           class=class="str">"cmt">//  Server certificate info
class="kw">public:
                     CSocket();
                    ~CSocket();
   class=class="str">"cmt">//--- methods to get class="kw">private properties

CSocket 类的接口与构造默认值

在 MT5 里封装 socket 通信,通常会写一个 CSocket 类把底层句柄和连接参数收进私有成员,再暴露一组 const 取参方法。下面这段声明能直接看到类对外暴露了什么:SocketID() 拿句柄、Address()/Port() 回连地址与端口、IsSecure() 看是否走 TLS,RxTimeout()/TxTimeout() 回读写超时。 静态成员 m_usedsockets 记录当前实例数,SocketsInUse() 静态方法返回它,方便你确认 EA 里到底开过几个 socket。Open() 的默认 use_tls=false、enablelog=false,说明不加密、不写日志是出厂态,真要接外网行情得自己显式开 TLS。 构造函数给的超时默认都是 150 毫秒:m_rx_timeout(150) 与 m_tx_timeout(150)。这个量级在本地回环或局域网够用,但外汇/贵金属实盘连远端行情商,150ms 超时可能偏紧,连不上时优先怀疑这里。 读一下核心声明与构造,就能照抄出自己的类壳:

MQL5 / C++
  class="type">int                SocketID(class="type">void)                 class="kw">const { class="kw">return(m_socket); }
  class="type">class="kw">string             Address(class="type">void)                  class="kw">const { class="kw">return(m_address);   }
  class="type">uint               Port(class="type">void)                    class="kw">const { class="kw">return(m_port);     }
  class="type">bool               IsSecure(class="type">void)                class="kw">const { class="kw">return(m_usetls); }
  class="type">uint               RxTimeout(class="type">void)               class="kw">const { class="kw">return(m_rx_timeout); }
  class="type">uint               TxTimeout(class="type">void)               class="kw">const { class="kw">return(m_tx_timeout); }
  class="type">bool               ServerCertificate(CERT& certificate);
  class=class="str">"cmt">//--- methods to set class="kw">private properties
  class="type">bool               SetTimeouts(class="type">uint tx_timeout, class="type">uint rx_timeout);
  class=class="str">"cmt">//--- general methods for working sockets
  class="type">void               Log(class="kw">const class="type">class="kw">string custom_message,class="kw">const class="type">int line,class="kw">const class="type">class="kw">string func);
  class="kw">static class="type">uint        SocketsInUse(class="type">void)            {  class="kw">return(m_usedsockets);  }
  class="type">bool               Open(class="kw">const class="type">class="kw">string server,class="type">uint port,class="type">uint timeout,class="type">bool use_tls=false,class="type">bool enablelog=false);
  class="type">bool               Close(class="type">void);
  class="type">uint               Readable(class="type">void);
  class="type">bool               Writable(class="type">void);
  class="type">bool               IsConnected(class="type">void);
  class="type">int                Read(class="type">uchar& out[],class="type">uint out_len,class="type">uint ms_timeout,class="type">bool read_available);
  class="type">int                Send(class="type">uchar& in[],class="type">uint in_len);
};
class="type">int CSocket::m_usedsockets=class="num">0;
CSocket::CSocket():m_socket(INVALID_HANDLE),
  m_address(""),
  m_port(class="num">0),
  m_usetls(false),
  m_log(false),
  m_rx_timeout(class="num">150),
  m_tx_timeout(class="num">150)
  {

◍ 连接前的清理与超时证书处理

在 MT5 里封装 socket 类,析构函数先判句柄:若 m_socket 不等于 INVALID_HANDLE 就调 Close(),避免退出 EA 时残留系统套接字。 SetTimeouts() 接收 tx_timeout 与 rx_timeout 两个毫秒级参数,内部走 SocketTimeouts() 系统函数;设置成功会把数值写回成员变量并打日志,失败直接返 false。实际跑行情中继时,把发送超时设 1000ms、接收设 3000ms 能压住多数卡连。 ServerCertificate() 用 SocketTlsCertificate() 抓取服务端 cert_subject、cert_issuer、cert_serial、cert_thumbprint、cert_expiry 五项,填进 m_cert 结构体后回传。未建连时调它必然返 false 并记『Invalid socket』。 Open() 开头有硬限制:m_usedsockets 达到 128 直接拒绝并日志报『Too many sockets open』。这意味着同终端并发外连上限约 128,做多账号聚合时要算好额度。外汇与贵金属接口调用属高风险操作,参数误设可能导致连接泄漏。

MQL5 / C++
  ZeroMemory(m_cert);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSocket::~CSocket()
  {
class=class="str">"cmt">//--- check handle
   if(m_socket!=INVALID_HANDLE)
      Close();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| set system socket timeouts                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSocket::SetTimeouts(class="type">uint tx_timeout,class="type">uint rx_timeout)
  {
   if(m_socket==INVALID_HANDLE)
     {
       Log("Invalid socket",__LINE__,__FUNCTION__);
       class="kw">return(false);
     }
   if(SocketTimeouts(m_socket,tx_timeout,rx_timeout))
     {
       m_tx_timeout=tx_timeout;
       m_rx_timeout=rx_timeout;
       Log("Socket Timeouts set",__LINE__,__FUNCTION__);
       class="kw">return(true);
     }
   class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| certificate                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSocket::ServerCertificate(CERT& certificate)
  {
   if(m_socket==INVALID_HANDLE)
     {
       Log("Invalid socket",__LINE__,__FUNCTION__);
       class="kw">return(false);
     }
   if(SocketTlsCertificate(m_socket,m_cert.cert_subject,m_cert.cert_issuer,m_cert.cert_serial,m_cert.cert_thumbprint,m_cert.cert_expiry))
     {
       certificate=m_cert;
       Log("Server certificate retrieved",__LINE__,__FUNCTION__);
       class="kw">return(true);
     }
   class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|connect()                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSocket::Open(class="kw">const class="type">class="kw">string server,class="type">uint port,class="type">uint timeout,class="type">bool use_tls=false,class="type">bool enablelog=false)
  {
   if(m_socket!=INVALID_HANDLE)
      Close();
   if(m_usedsockets>=class="num">128)
     {
       Log("Too many sockets open",__LINE__,__FUNCTION__);
       class="kw">return(false);
     }
   m_usetls=use_tls;
   m_log=enablelog;

「连接与断开时的套接字底层动作」

在 MT5 里用 MQL5 做行情转发或 AIGC 接口调用,第一步往往是 SocketCreate。若返回 INVALID_HANDLE,说明终端的 socket 资源没拿到,这时候直接写日志并退出,比硬连更稳妥。 端口分配有个隐藏逻辑:port 传 0 时,启用 TLS 就默认走 443,否则落回 80;而如果没开 TLS 却填了 443,代码会强制把 m_usetls 置 true。这种自动纠正在回测环境不易暴露,但在真实 VPS 上可能让明文请求意外走加密握手。 Close() 不是简单释放句柄。SocketClose 成功后要把 m_usedsockets 减 1、地址清空、证书内存归零,否则下一次重连可能带着旧会话状态。外汇与贵金属接口调用涉及网络传输,存在断连与重连失败的高风险,务必在 EA 里捕获返回值。 Readable() 仅做前置校验:句柄无效直接返 0,不抛异常。这意味着调用方得自己判断 0 是「无数据」还是「连接已死」,不能盲目轮询。

MQL5 / C++
  m_socket=SocketCreate();
  if(m_socket==INVALID_HANDLE)
    {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      class="kw">return(false);
    }
  ++m_usedsockets;
  m_address=server;
  if(port==class="num">0)
    {
      if(m_usetls)
        m_port=class="num">443;
      else
        m_port=class="num">80;
    }
  else
      m_port=port;
class=class="str">"cmt">//---
  if(!m_usetls && m_port==class="num">443)
      m_usetls=true;
class=class="str">"cmt">//---
  Log("Connecting to "+m_address,__LINE__,__FUNCTION__);
class=class="str">"cmt">//---
  if(m_usetls)
    {
      if(m_port!=class="num">443)
        {
          if(SocketConnect(m_socket,server,port,timeout))
              class="kw">return(SocketTlsHandshake(m_socket,server));
        }
      else
        {
          class="kw">return(SocketConnect(m_socket,server,port,timeout));
        }
    }
  class="kw">return(SocketConnect(m_socket,server,port,timeout));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|close()                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSocket::Close(class="type">void)
  {
class=class="str">"cmt">//---
  if(m_socket==INVALID_HANDLE)
    {
      Log("Socket Disconnected",__LINE__,__FUNCTION__);
      class="kw">return(true);
    }
class=class="str">"cmt">//---
  if(SocketClose(m_socket))
    {
      m_socket=INVALID_HANDLE;
      --m_usedsockets;
      Log("Socket Disconnected from "+m_address,__LINE__,__FUNCTION__);
      m_address="";
      ZeroMemory(m_cert);
      class="kw">return(true);
    }
class=class="str">"cmt">//---
  Log("",__LINE__,__FUNCTION__);
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|readable()                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint CSocket::Readable(class="type">void)
  {
  if(m_socket==INVALID_HANDLE)
    {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      class="kw">return(class="num">0);
    }
class=class="str">"cmt">//---

可读可写与收发前的状态闸门

在 MT5 的 CSocket 封装里,Readable、Writable、IsConnected 三个方法本质都是对底层句柄状态的探活,而不是真正收发数据。它们先统一判断 m_socket 是否等于 INVALID_HANDLE,若无效就打日志并返回 false,避免后续调用炸在系统 API 上。 Readable 直接转发 SocketIsReadable(m_socket),Writable 对应 SocketIsWritable(m_socket),IsConnected 对应 SocketIsConnected(m_socket)。这三个布尔值给 EA 在循环里做「先问后发」的节流逻辑用,能减少无效阻塞调用。 真正的读逻辑在 Read() 里分层:如果开了 m_usetls,按 read_available 标志走 SocketTlsReadAvailable 或 SocketTlsRead;没开 TLS 就走普通 SocketRead 并传入 ms_timeout 超时(单位毫秒)。当句柄无效时 Read 返回 -1,调用方必须判这个值,不能当长度用。 开 MT5 把这段塞进你自己的 socket 类,故意传一个 INVALID_HANDLE 进去,能看到 Log 打出「Invalid socket」且方法安全退出,不会让策略线程直接挂掉。外汇与贵金属行情推送走 socket 时延迟敏感,这类状态闸门可降低断线重连期间的异常概率,但网络风险仍高,需配合重连机制。

MQL5 / C++
class="type">bool CSocket::Writable(class="type">void)
  {
  if(m_socket==INVALID_HANDLE)
    {
    Log("Invalid socket",__LINE__,__FUNCTION__);
    class="kw">return(false);
    }
class=class="str">"cmt">//---
  Log("Is Socket Writable ",__LINE__,__FUNCTION__);
class=class="str">"cmt">//---
  class="kw">return(SocketIsWritable(m_socket));
  }
class="type">bool CSocket::IsConnected(class="type">void)
  {
  if(m_socket==INVALID_HANDLE)
    {
    Log("Invalid socket",__LINE__,__FUNCTION__);
    class="kw">return(false);
    }
class=class="str">"cmt">//---
  Log("Is Socket Connected ",__LINE__,__FUNCTION__);
class=class="str">"cmt">//---
  class="kw">return(SocketIsConnected(m_socket));
  }
class="type">int CSocket::Read(class="type">uchar& out[],class="type">uint out_len,class="type">uint ms_timeout,class="type">bool read_available=false)
  {
  if(m_socket==INVALID_HANDLE)
    {
    Log("Invalid socket",__LINE__,__FUNCTION__);
    class="kw">return(-class="num">1);
    }
class=class="str">"cmt">//---
  Log("Reading from "+m_address,__LINE__,__FUNCTION__);
  if(m_usetls)
    {
    if(read_available)
      class="kw">return(SocketTlsReadAvailable(m_socket,out,out_len));
    else
      class="kw">return(SocketTlsRead(m_socket,out,out_len));
    }
  else
    class="kw">return(SocketRead(m_socket,out,out_len,ms_timeout));
  class="kw">return(-class="num">1);
  }

◍ Send 与 Log 两个方法的落点

CSocket 的 Send 方法先判句柄:若 m_socket 等于 INVALID_HANDLE,直接打一条 Invalid socket 日志并返回 -1,避免对未建连的套接字做写操作。 如果句柄有效,方法会先打印「Sending to <地址>」的走向信息,再按 m_usetls 开关分流——为真走 SocketTlsSend,否则走 SocketSend,两者都把同一份 uchar 缓冲与长度塞进去。 Log 是内部诊断口:仅当 m_log 为真才干活。它先抓 GetLastError,若错误码非 0 就按「[MQL error ID: %d][消息][Line: %d][Function: %s]」格式输出并 ResetLastError;无错但有自定义消息时才印简化行。 在 MT5 里把这段直接贴进 EA 的 socket 类,把 m_log 设为 true,连一次行情中继服,就能在专家日志看到每次 Send 的地址与潜在错误 ID,省掉盲调。

MQL5 / C++
class="type">int CSocket::Send(class="type">uchar& in[],class="type">uint in_len)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      class="kw">return(-class="num">1);
     }
class=class="str">"cmt">//---
   Log("Sending to "+m_address,__LINE__,__FUNCTION__);
class=class="str">"cmt">//---
   if(m_usetls)
      class="kw">return(SocketTlsSend(m_socket,in,in_len));
   else
      class="kw">return(SocketSend(m_socket,in,in_len));
class=class="str">"cmt">//---
   class="kw">return(-class="num">1);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|log()                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSocket::Log(class="kw">const class="type">class="kw">string custom_message,class="kw">const class="type">int line,class="kw">const class="type">class="kw">string func)
  {
   if(m_log)
     {
      class=class="str">"cmt">//---
      class="type">int eid=GetLastError();
      class=class="str">"cmt">//---
      if(eid!=class="num">0)
        {
         PrintFormat("[MQL error ID: %d][%s][Line: %d][Function: %s]",eid,custom_message,line,func);
         ResetLastError();
         class="kw">return;
        }
      if(custom_message!="")
         PrintFormat("[%s][Line: %d][Function: %s]",custom_message,line,func);
     }
class=class="str">"cmt">//---
  }
让小布替你验证连通性
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 WebSocket 握手状态和延迟抖动,把重复劳动交给小布,你专注决策。

常见问题

长轮询仍依赖 HTTP 请求挂起,半双工且头部开销大;WebSocket 基于 TCP 全双工,建连后双向实时推数据,带宽与延迟都更优,外汇贵金属高频场景里概率上更稳。
CSocket 只封装了基础 TCP 收发,握手与帧解析需自己补。本文进阶篇聚焦如何在其上叠 WebSocket 逻辑,不依赖外部库。
可以,小布盯盘的品种页内置 AIGC 诊断,能识别握手失败与连接漂移并提示,省去你写日志轮询的功夫。
DLL 引入第三方二进制,升级与杀软兼容风险高。原生实现虽要手写协议层,但审计面小,实盘外汇贵金属本就高风险,少一层未知更稳妥。
按协议规范,客户端发往服务器的帧必须掩码,服务端发来不加。MQL5 封装时漏掉掩码会被对端拒连,属常见排错点。