MetaTrader 5 的 WebSocket  使用 Windows API·进阶篇
🪟

MetaTrader 5 的 WebSocket 使用 Windows API·进阶篇

(2/3)· 还在为 MQL5 套接字兼容性和外部库头疼?系统级方案把实时报价直送图表

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

很多交易者尝试在 MT5 里接实时数据时,卡在外部库版本冲突和老旧系统不支持。其实 Windows 8.1 之后系统已自带 WebSocket 能力,只是很少有人把它和 MQL5 打通。本文顺着上篇协议基础,直接进系统层实操。

「MT5 原生 WinHTTP WebSocket 接口签名」

在 MQL5 里做实时行情桥接,不用自己造 HTTP 底层,系统已经通过 WinHttp 的 import 段把 WebSocket 相关函数暴露出来了。上面这组声明直接挂在 kernel32 之外的 winhttp.dll 上,函数句柄类型统一是 HINTERNET,和常规 WinHTTP 请求复用同一套连接生命周期。 具体看这几个入口:WinHttpWebSocketCompleteUpgrade 负责在普通 HTTPS 响应后把连接升级成 WebSocket,第二个参数是输出型 DWORD 引用;WinHttpWebSocketSend 的第三个参数是 BYTE 数组引用加长度,发送前需自己填好二进制帧;WinHttpWebSocketReceive 则要求预先分配 BYTE 缓冲并传入容量,收盘数据长度与帧类型都由输出引用带回。 WinHttpWebSocketClose 与 QueryCloseStatus 成对出现,前者发关闭帧时带 ushort 状态码,后者回查对端关闭原因,做断线诊断时这两个必须都调,否则拿不到服务端踢人真实状态。外汇与贵金属行情走这些通道时延迟敏感,实盘前建议在 MT5 策略测试器外挂脚本里先跑通升级与收发,确认句柄不泄露再上真仓——这类接口出错多为静默返回非 0 DWORD,高风险。

MQL5 / C++
HINTERNET WinHttpWebSocketCompleteUpgrade(HINTERNET,DWORD&);
class="type">bool WinHttpCloseHandle(HINTERNET);
DWORD WinHttpWebSocketSend(HINTERNET,WINHTTP_WEB_SOCKET_BUFFER_TYPE,BYTE&[],DWORD);
DWORD WinHttpWebSocketReceive(HINTERNET,BYTE&[],DWORD,DWORD&,WINHTTP_WEB_SOCKET_BUFFER_TYPE&);
DWORD WinHttpWebSocketClose(HINTERNET,class="type">class="kw">ushort,BYTE&[],DWORD);
DWORD WinHttpWebSocketQueryCloseStatus(HINTERNET,class="type">class="kw">ushort&,BYTE&[],DWORD,DWORD&);
class="macro">#class="kw">import

◍ 从 WinHttpOpen 到 WebSocket 句柄的链路

在 MT5 里用 winhttp 搭 WebSocket 客户端,第一步永远是 WinHttpOpen(),它初始化整个函数库并返回一个会话句柄,后续所有调用都靠这个句柄串联。跳过这步直接连服务器,句柄为空会立刻在 GetLastError() 里报错。 第二步 WinHttpConnect() 只吃裸域名或 IP,不吃协议头和路径。比如完整地址是 wss://ws.example.com/path,这里只传 ws.example.com;多数连接失败都源于把 wss:// 或 /path 也塞了进去。 拿到连接句柄后调 WinHttpOpenRequest() 建请求句柄,路径组件(如 /path)在此传入,并用 WINHTTP_FLAG_SECURE 控制是否走 TLS。随后 WinHttpSetOption() 带 WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET 写进升级标记,再由 WinHttpSendRequest() + WinHttpReceiveResponse() 发起握手。 WinHttpWebSocketCompleteUpgrade() 校验响应是否符合 WebSocket 协议,通过才返回真正的 WebSocket 句柄。此时请求句柄已无用,应调 WinHttpCloseHandle() 释放;之后收发只用 WinHttpWebSocketSend() / WinHttpWebSocketReceive()。关闭时先 WinHttpWebSocketClose(),再逐级 CloseHandle 还原所有句柄。 下面这段脚本骨架把上述链路落到了可编译代码,server、Port、path、ExtTLS 需自行补定义。注意每个失败分支都按序释放已建句柄,否则 MT5 终端会漏句柄。

MQL5 / C++
class="macro">#include<winhttp.mqh>
HINTERNET sessionhandle,connectionhandle,requesthandle,websockethandle;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   sessionhandle=connectionhandle=requesthandle=websockethandle=NULL;
   sessionhandle=WinHttpOpen("MT5 app",WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,class="num">0);
   if(sessionhandle==NULL)
     {
      Print("WinHttpOpen error" +class="type">class="kw">string(kernel32::GetLastError()));
      class="kw">return;
     }
connectionhandle=WinHttpConnect(sessionhandle,server,Port,class="num">0);
   if(connectionhandle==NULL)
     {
      Print("WinHttpConnect error "+class="type">class="kw">string(kernel32::GetLastError()));
      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);
      class="kw">return;
     }
requesthandle=WinHttpOpenRequest(connectionhandle,"GET",path,NULL,NULL,NULL,(ExtTLS)?WINHTTP_FLAG_SECURE:class="num">0);
   if(requesthandle==NULL)
     {
      Print("WinHttpOpenRequest error "+class="type">class="kw">string(kernel32::GetLastError()));
      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);
      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);
      class="kw">return;
     }
class="type">uint nullpointer[]= {};
   if(!WinHttpSetOption(requesthandle,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,class="num">0))
     {
      Print("WinHttpSetOption upgrade error "+class="type">class="kw">string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
         WinHttpCloseHandle(requesthandle);
      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);
      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);
      class="kw">return;
     }
if(!WinHttpSendRequest(requesthandle,NULL,class="num">0,nullpointer,class="num">0,class="num">0,class="num">0))
     {
      Print("WinHttpSendRequest error "+class="type">class="kw">string(kernel32::GetLastError()));

WebSocket 握手收尾与收发封装

在完成 HTTP 响应接收后,需要把普通请求句柄升级成 WebSocket 句柄。下面这段逻辑先判断 WinHttpReceiveResponse 是否成功,失败就打印 kernel32::GetLastError() 的错误码并逐级关闭 request、connection、session 三个句柄后退出,避免句柄泄漏。 升级动作靠 WinHttpWebSocketCompleteUpgrade(requesthandle, nv) 实现,nv 为 0 的 ulong 上下文参数。若返回 NULL 同样走三段式 CloseHandle 并 return,成功后才把原 requesthandle 关闭并置空,后续收发只用 websockethandle。 发送函数 WebsocketSend 先把字符串转成 BYTE 数组,用 ArrayRemove 削掉末尾的 \0 终止符(WHOLE_ARRAY 转换会多带一个),再以 WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE 类型发出。WinHttpWebSocketSend 返回 0 表示成功,所以函数里 send 非零反而 return(false),这个真假颠倒要留意。 接收端 WebSocketRecv 预分配 65539 字节的 rbuffer(比 64KB 多 3 字节头部余量),用 do-while 循环反复 WinHttpWebSocketReceive 直到拿完。每次 transferred 累加进 done,ArrayCopy 拼接到 rxbuffer,called 计数可用来观察一次完整消息被拆成了几次底层读取。外汇与贵金属行情走 WebSocket 推送时延迟低但断连风险高,实盘使用前建议在 MT5 策略测试器外用真实账户环境验证句柄释放是否干净。

MQL5 / C++
if(requesthandle!=NULL)
   WinHttpCloseHandle(requesthandle);
if(connectionhandle!=NULL)
   WinHttpCloseHandle(connectionhandle);
if(sessionhandle!=NULL)
   WinHttpCloseHandle(sessionhandle);
class="kw">return;
   }
 if(!WinHttpReceiveResponse(requesthandle,nullpointer))
   {
     Print("WinHttpRecieveResponse no response "+class="type">class="kw">string(kernel32::GetLastError()));
     if(requesthandle!=NULL)
       WinHttpCloseHandle(requesthandle);
     if(connectionhandle!=NULL)
       WinHttpCloseHandle(connectionhandle);
     if(sessionhandle!=NULL)
       WinHttpCloseHandle(sessionhandle);
     class="kw">return;
   }
class="type">ulong nv=class="num">0;
   websockethandle=WinHttpWebSocketCompleteUpgrade(requesthandle,nv);
   if(websockethandle==NULL)
   {
     Print("WinHttpWebSocketCompleteUpgrade error "+class="type">class="kw">string(kernel32::GetLastError()));
     if(requesthandle!=NULL)
       WinHttpCloseHandle(requesthandle);
     if(connectionhandle!=NULL)
       WinHttpCloseHandle(connectionhandle);
     if(sessionhandle!=NULL)
       WinHttpCloseHandle(sessionhandle);
     class="kw">return;
   }
   WinHttpCloseHandle(requesthandle);
   requesthandle=NULL;
class="type">bool WebsocketSend(const class="type">class="kw">string message)
  {
  BYTE msg_array[];
  StringToCharArray(message,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="type">ulong send=WinHttpWebSocketSend(websockethandle,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,msg_array,len);
  if(send)
      class="kw">return(false);
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool WebSocketRecv(class="type">uchar &rxbuffer[],class="type">ulong &bytes_read)
  {
  WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype=-class="num">1;
  BYTE rbuffer[class="num">65539];
  class="type">ulong rbuffersize=class="type">ulong(ArraySize(rbuffer));
  class="type">ulong done=class="num">0;
  class="type">ulong transferred=class="num">0;
  ZeroMemory(rxbuffer);
  ZeroMemory(rbuffer);
  bytes_read=class="num">0;
  class="type">int called=class="num">0;
  do
    {
      called++;
      class="type">ulong get=WinHttpWebSocketReceive(websockethandle,rbuffer,rbuffersize,transferred,rbuffertype);
      if(get)
        {
         class="kw">return(false);
        }
      ArrayCopy(rxbuffer,rbuffer,(class="type">int)done,class="num">0,(class="type">int)transferred);
      done+=transferred;
      transferred=class="num">0;

「WebSocket 收尾与句柄清理」

这段逻辑处在 WebSocket 数据接收循环的末尾,先通过 WinHttpWebSocketClose 以成功状态码正常关闭套接字,传入的 closearray 长度为 0,表示不带自定义关闭载荷。 若 close 返回值非零,说明关闭过程出错,此时用 kernel32::GetLastError 打印系统错误码,并依次判断 requesthandle、websockethandle、connectionhandle、sessionhandle 是否非空,逐个调用 WinHttpCloseHandle 释放,避免句柄泄漏。 实际在 MT5 里跑这类 WinHTTP 封装时,句柄未关闭会让后续重连概率性失败;建议在日志里记录每次 close 的返回值,确认 0 才视为干净退出。

MQL5 / C++
BYTE closearray[]= {};
  class="type">ulong close=WinHttpWebSocketClose(websockethandle,WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,closearray,class="num">0);
  if(close)
    {
      Print("websocket close error "+class="type">class="kw">string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
        WinHttpCloseHandle(requesthandle);
      if(websockethandle!=NULL)
        WinHttpCloseHandle(websockethandle);
      if(connectionhandle!=NULL)
        WinHttpCloseHandle(connectionhandle);
      if(sessionhandle!=NULL)
        WinHttpCloseHandle(sessionhandle);
      class="kw">return;
    }

◍ CWebsocket 类的连接与收发骨架

在 MT5 里跑 WebSocket 客户端,核心就是 websocket.mqh 里封装的 CWebsocket 类。它用 winhttp 库做底层,文件开头先 include winhttp.mqh,把 Windows API 的函数和声明一次性导进来,省得逐个声明。 起手调用 Connect(),四个参数要填清楚:_serveraddress 是完整服务器地址(string),_port 是端口号(ushort),_appname 是标识客户端的字符串、会作为初始 HTTP 请求头发出去,_secure 是布尔值决定走不走安全连接。Connect() 内部会先后调私有的 initialize() 和 upgrade()——前者拆地址成域名和路径,后者建请求和 WebSocket 句柄。 连上了(返回 true)才能发数据。SendString() 吃字符串,Send() 吃无符号字符数组,成功都返 true,实际都走私有的 clientsend()。读数据用 Read() 或 ReadString(),返回接收字节数;ReadString() 通过引用写字符串,Read() 写无符号字符数组。 不用了就 Close() 或 Abort()。Abort() 不光断连,还会把部分类属性重置回默认。ClientState() 查当前状态,DomainName()/Port()/ServerPath() 回连组件,LastErrorMessage() 和 LastError() 分别给错误文本和整型码。 下面这段是类里 Connect 与 initialize 的实体代码,注意默认端口逻辑:_port 传 0 时按 isSecure 自动补 443 或 80,传 443 且 _secure 漏写也会强制安全。

MQL5 / C++
class="macro">#include<winhttp.mqh>
class="macro">#define WEBSOCKET_ERROR_FIRST            WINHTTP_ERROR_LAST+class="num">1000
class="macro">#define WEBSOCKET_ERROR_NOT_INITIALIZED    WEBSOCKET_ERROR_FIRST+class="num">1
class="macro">#define WEBSOCKET_ERROR_EMPTY_SEND_BUFFER  WEBSOCKET_ERROR_FIRST+class="num">2
class="macro">#define WEBSOCKET_ERROR_NOT_CONNECTED      WEBSOCKET_ERROR_FIRST+class="num">3
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| websocket state enumeration                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_WEBSOCKET_STATE
  {
   CLOSED = class="num">0,
   CLOSING,
   CONNECTING,
   CONNECTED
  };
class="type">bool CWebsocket::Connect(const class="type">class="kw">string _serveraddress, const INTERNET_PORT _port=class="num">443, const class="type">class="kw">string _appname=NULL,class="type">bool _secure=true)
  {
   if(clientState==CONNECTED)
     {
      if(StringCompare(_serveraddress,serveraddress,false))
         Abort();
      else
         class="kw">return(true);
     }
   if(!initialize(_serveraddress,_port,appname,_secure))
      class="kw">return(false);
   class="kw">return(upgrade());
  }
class="type">bool CWebsocket::initialize(const class="type">class="kw">string _serveraddress,const class="type">class="kw">ushort _port,const class="type">class="kw">string _appname,class="type">bool _secure)
  {
   if(initialized)
      class="kw">return(true);
   if(_secure)
      isSecure=true;
   if(_port==class="num">0)
     {
      if(isSecure)
         serverPort=class="num">443;
      else
         serverPort=class="num">80;
     }
   else
     {
      serverPort=_port;
      isSecure=_secure;
      if(serverPort==class="num">443 && !isSecure)
         isSecure=true;
     }
   if(_appname!=NULL)
      appname=_appname;
   else
      appname="Mt5 app";
   serveraddress=_serveraddress;
   class="type">int dot=StringFind(serveraddress,".");

从地址解析到 WebSocket 握手落地

这段逻辑紧接前面初始化入口,先把传入的 serveraddress 拆成名称、端口与路径三段。dot 定位首个斜杠,ss 据此切出 serverPath;若没找到就回退为根路径「/」。 sss 用来找「://」协议头,找不到时故意把下标设为 -3,使后面 StringSubstr 从地址开头偏移 3 取 serverName,等于跳过不存在的协议头。随后调用 createSessionConnection 建立会话并返回初始化结果。 createSessionConnection 里先用 WinHttpOpen 拿 hSession,失败就写错误描述并返 false;接着 WinHttpConnect 用 serverName 与 serverPort 连服务器。注意源码中第二处判错写的是 if(hSession==NULL) 而非 hConnection,这属于笔误,实盘复制时建议改成判 hConnection,否则连接句柄为空也不会被捕获。 upgrade 方法发一次 GET 并置 WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,完成握手后 WinHttpWebSocketCompleteUpgrade 拿到 hWebSocket,关掉原 hRequest,状态切到 CONNECTED。clientsend 在发送前检查数组长度,空缓冲直接报 WEBSOCKET_ERROR_EMPTY_SEND_BUFFER,再调 WinHttpWebSocketSend 以二进制消息类型外发。外汇与贵金属行情转发走这套通道时波动大、断连概率高,务必在 EA 里加重连与心跳。

MQL5 / C++
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);
 initialized=createSessionConnection();
 class="kw">return(initialized);
}
class="type">bool CWebsocket::createSessionConnection(class="type">void)
 {
 hSession=WinHttpOpen(appname,WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,class="num">0);
 if(hSession==NULL)
 {
 setErrorDescription();
 class="kw">return(false);
 }
 hConnection=WinHttpConnect(hSession,serverName,serverPort,class="num">0);
 if(hSession==NULL)
 {
 setErrorDescription();
 reset();
 class="kw">return(false);
 }
 class="kw">return(true);
}
class="type">bool CWebsocket::upgrade(class="type">void)
 {
 clientState=CONNECTING;
 hRequest=WinHttpOpenRequest(hConnection,"GET",serverPath,NULL,NULL,NULL,(isSecure)?WINHTTP_FLAG_SECURE:class="num">0);
 if(hRequest==NULL)
 {
 setErrorDescription();
 reset();
 class="kw">return(false);
 }
 class="type">uint nullpointer[]= {};
 if(!WinHttpSetOption(hRequest,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,class="num">0))
 {
 setErrorDescription();
 reset();
 class="kw">return(false);
 }
 if(!WinHttpSendRequest(hRequest,NULL,class="num">0,nullpointer,class="num">0,class="num">0,class="num">0))
 {
 setErrorDescription();
 reset();
 class="kw">return(false);
 }
 if(!WinHttpReceiveResponse(hRequest,nullpointer))
 {
 setErrorDescription();
 reset();
 class="kw">return(false);
 }
 class="type">ulong nv=class="num">0;
 hWebSocket=WinHttpWebSocketCompleteUpgrade(hRequest,nv);
 if(hWebSocket==NULL)
 {
 setErrorDescription();
 reset();
 class="kw">return(false);
 }
 WinHttpCloseHandle(hRequest);
 hRequest=NULL;
 clientState=CONNECTED;
 class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| helper method for sending data to the server |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWebsocket::clientsend(BYTE &txbuffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype)
 {
 DWORD len=(ArraySize(txbuffer));
 if(len<=class="num">0)
 {
 setErrorDescription(WEBSOCKET_ERROR_EMPTY_SEND_BUFFER);
 class="kw">return(false);
 }
 class="type">ulong send=WinHttpWebSocketSend(hWebSocket,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,txbuffer,len);
 if(send)
把连接诊断交给小布
这些 Windows API 调用状态和报价延迟诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需关心策略逻辑。

常见问题

WinINet 偏通用互联网协议,WebSocket 支持不完整;WinHTTP 自 Windows 8.1 起原生暴露 WebSocket 函数,更适合系统级客户端实现。
可以,小布盯盘内置的 AIGC 会标记连接异常与推送延迟,省去你手动打印调试信息的重复劳动。
该系统未提供原生 WebSocket 协议支持,相关函数不存在,程序无法完成升级握手,需升级到 8.1 或以上。
类内部用 WinHttpWebSocketSend 发订阅帧,Receive 拿推送,解析后投喂图表,具体字段映射见案例拆解节。