MetaTrader 5 中的 WebSockets·进阶篇
(2/3)· 轮询烧带宽、长连接空耗成本,MQL5 原生集成 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 毫秒区间再压测。
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 超时可能偏紧,连不上时优先怀疑这里。 读一下核心声明与构造,就能照抄出自己的类壳:
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,做多账号聚合时要算好额度。外汇与贵金属接口调用属高风险操作,参数误设可能导致连接泄漏。
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 是「无数据」还是「连接已死」,不能盲目轮询。
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 时延迟敏感,这类状态闸门可降低断线重连期间的异常概率,但网络风险仍高,需配合重连机制。
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,省掉盲调。
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">//--- }