用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接(基础篇)
📘

用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接(基础篇)

第 1/3 篇

◍ 在 MT5 里用 Windows API 搭异步 WebSocket 客户端

MT5 原生不提供 WebSocket 支持,但交易终端跑在 Windows 上,可以直接调 WinAPI 的 Wininetwebsocket 相关函数做异步连接,把行情或信号源推流进 EA。 实际可行路径是:先用 InternetOpen 建会话,再用 InternetConnect 指向 ws 地址,最后用 InternetWriteFile / InternetReadFile 收发帧。这样 EA 不阻塞主线程,Tick 处理不受影响。 一篇 2026-01-08 发布的示例帖实测在 MT5 build 4410 上完成握手延迟约 120–180 ms(局域网 ws 服务),公网节点会到 300 ms 以上,高频策略要自己留余量。外汇与贵金属杠杆高,接外部流若断线重连没写好,可能错单或重复开仓,风险自担。

为什么同步 WebSocket 不够用

之前在 MT5 里用 Windows API 搭 WebSocket 客户端的方案,本质跑在同步模式上:调用阻塞,EA 或指标主线程会被网络往返拖住,行情刷新和订单逻辑都得排队。对毫秒级盯盘来说,这种卡顿不可接受。 本文换一条路:写一个自定义 DLL,把连接、收发都丢到异步线程里,再暴露一组给 MQL5 直接调用的导出函数。这样 MT5 主线程只管发指令、读结果,不再被 socket 阻塞。 后面会拆 DLL 的开发过程,并给一个可跑的 MT5 调用示例。你手上有 VS 和 MT5 就能照着编译验证,贵金属和外汇品种上跑异步行情订阅前,先记住这类外部 DLL 调用属高风险扩展,实盘前务必在模拟盘压测。

「MT5里跑WinHTTP异步要先过这两关」

想在 MT5 的 DLL 里用 WinHTTP 做异步收发,第一步是 WinHttpOpen 时给会话句柄挂上 WINHTTP_FLAG_ASYNC 或 WINHTTP_FLAG_SECURE_DEFAULTS。少了这个标识,后面再怎么注册回调都是同步逻辑,网络阻塞会直接卡住 EA 的主线程。 会话句柄建好之后,要用 WinHttpSetStatusCallback 挂一个状态回调函数,并指定想接收哪些通知标识——可以全量订阅 WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS,也可以只挑部分事件。实测中会话句柄建完立刻注册并非强制,WebSocket 连接前或中途对任意有效 HINTERNET 句柄调用都来得及。 文档说回调签名里带一个 dwContext 参数(内容值),理论上靠 WinHttpSetOption 设 WINHTTP_OPTION_CONTEXT_VALUE 就能从回调里取回自定义结构。但我们多次实测,这种方式稳定取回已注册内容值经常失败,最后只能退一步用全局变量顶上,具体写法留到 DLL 实现那节拆。 线程安全本来是回调函数的硬约束,不过 MT5 程序本质是单线程模型,DLL 虽跑在宿主进程的线程池里,实际活动的就那一条线程,所以这层限制可以放宽,不用像通用 Windows 服务那样加锁。 下面这段是最小可验证骨架:开会话、设上下文、挂全量完成通知,回调原型也一并列出,直接塞进 VS 的 DLL 工程就能编译看行为。

MQL5 / C++
class=class="str">"cmt">// Set hSession
hSession = WinHttpOpen(L"MyApp", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
if (hSession == NULL)
      ErrorCode = ERROR_INVALID_HANDLE;
class=class="str">"cmt">// Return error code
class="kw">return ErrorCode;
if (!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, class="kw">sizeof(this)))
{
      class=class="str">"cmt">// Handle error
      ErrorCode = GetLastError();
      class="kw">return ErrorCode;
}
if (WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, class="num">0) == WINHTTP_INVALID_STATUS_CALLBACK)
{
      ErrorCode = GetLastError();
      class="kw">return ErrorCode;
}
class="type">void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)

◍ 用 C++ 给 MT5 搭一个 WinHTTP WebSocket 通道

想在 MT5 里直连行情或桥接服务,最稳的做法是把 WebSocket 客户端塞进一个 DLL,再用 MQL5 调导出的包装函数。这套 DLL 要在 Visual Studio 装好「C++ 桌面开发」工作负载,并配上 Windows 10/11 SDK——SDK 提供 WinHTTP 的 .lib,编译时得链进去。 DLL 至少含三块:封装 WinHTTP WebSocket 核心的 WebSocketClient 类、一个配合全局映射表 clients 的回调函数、以及带 WEBSOCK_API 限定符的导出函数包装器。头文件 asyncwebsocketclient.h 里先声明类和 Frame 结构体、ENUM_WEBSOCKET_STATE 枚举,再把 clients 做成全局 map,让任意回调函数都能按 HINTERNET 句柄捞到对应实例。 回调里只挑「完成通知」处理:WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE 代表发完,WINHTTP_CALLBACK_STATUS_READ_COMPLETE 代表读到,状态写回实例内部字段。OnReadComplete() 把原始字节推进帧队列,OnSendComplete() 翻发送标志,OnError() 接错误码。 外部建连走 client_connect(),传地址、端口、是否安全、HINTERNET 指针;出错返非 0 且句柄置 NULL。内部 new 一个 WebSocketClient,Connect() 先同步握手,再 EnableCallBack() 挂异步。连上后 client_send() 只是排产,真结果靠回调通知。 收数据是被动的:client_poll() 触发一次 WinHttpWebSocketReceive(),读完即停监听,想再收必须重调。client_read() 是从内部队列同步取帧,出队即删;client_readable() 探队首大小。断线用 client_disconnect(),随后 client_reset() 清内存缓冲——不强制,但漏掉可能拖住后续资源。 下面这段是 WebSocketClient 类的私有成员声明,句柄与计数都在这一层落地: class WebSocketClient { private: HINTERNET hSession; // 会话句柄 HINTERNET hConnect; // 连接句柄 HINTERNET hRequest; // 握手用的 HTTP 请求句柄 HINTERNET hWebSocket; // WebSocket 句柄 DWORD initialized; // 初始化标志 DWORD bytesTX; // 已发字节数 DWORD ErrorCode; // 最近错误码 DWORD completed_websocket_operation; // 回调上报的最近完成操作 外汇与贵金属杠杆高、滑点大,直连WebSocket虽快,断线重连逻辑若漏了 client_reset() 可能让旧缓冲区混入新会话,实盘前请在策略测试器外另开 Demo 账户验证 DLL 加载与句柄生命周期。

MQL5 / C++
class WebSocketClient {
class="kw">private:
    HINTERNET hSession;
    HINTERNET hConnect;
    HINTERNET hRequest;
    HINTERNET hWebSocket;
    DWORD initialized;
    DWORD bytesTX;
    DWORD ErrorCode;
    DWORD completed_websocket_operation;

WebSocketClient 的底层成员与接口轮廓

在 MT5 里做实时行情桥接,绕不开一个封装好的 WebSocketClient 类。它的私有段先挂了一个 std::queue<Frame>* frames,用来缓存服务端推过来的帧队列;同时用 ENUM_WEBSOCKET_STATE status 标记客户端状态,这两个字段决定了你后续读数据前要不要先判断链路活着。 初始化与复位接口很直白:DWORD Initialize(VOID) 负责拿到 hSession 句柄,VOID Reset(bool reset_error = true) 做对象状态清零,默认连内部错误缓冲一起清。注意拷贝构造和赋值运算符全部被 = delete,说明这个类禁止值语义拷贝,只能以引用或指针形式在 EA 里长期持有。 公开接口暴露了链路全周期:Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure) 用 host+port+secure 三参建连,返回 DWORD 错误码,0 为成功;Send(...) 按 WINHTTP_WEB_SOCKET_BUFFER_TYPE 发帧;还有 WebSocketHandle() 回传 hWebSocket 句柄供底层识别。字节侧有 DWORD bytesRX 和 std::vector<BYTE> rxBuffer 接数据,rxBufferType 标帧类型。 实跑时先确认 Connect 返回 0 再调 Send,否则 MT5 终端可能静默丢帧;外汇与贵金属实时流受网络与经纪商限制,断线重连概率不低,务必用 status 做守卫。

MQL5 / C++
class=class="str">"cmt">//internal queue of frames sent from a server
std::queue<Frame>* frames;
class=class="str">"cmt">//client state;
ENUM_WEBSOCKET_STATE status;
class=class="str">"cmt">//sets an hSession handle
DWORD Initialize(VOID);
class=class="str">"cmt">// reset state of object
class=class="str">"cmt">/*
 reset_error: boolean flag indicating whether to rest the
 internal error buffers.
*/
VOID  Reset(class="type">bool reset_error = true);

class="kw">public:
class=class="str">"cmt">//constructor(s)
WebSocketClient(VOID);
WebSocketClient(const WebSocketClient&) = class="kw">delete;
WebSocketClient(WebSocketClient&&) = class="kw">delete;
WebSocketClient& class="kw">operator=(const WebSocketClient&) = class="kw">delete;
WebSocketClient& class="kw">operator=(WebSocketClient&&) = class="kw">delete;
class=class="str">"cmt">//destructor
~WebSocketClient(VOID);
class=class="str">"cmt">//received bytes;
DWORD bytesRX;
class=class="str">"cmt">// receive buffer
std::vector<BYTE> rxBuffer;
class=class="str">"cmt">// received frame type;
WINHTTP_WEB_SOCKET_BUFFER_TYPE rxBufferType;
class=class="str">"cmt">// Get the winhttp websocket handle
class=class="str">"cmt">/*
class="kw">return: returns the hWebSocket handle which is used to
identify a websocket connection instance
*/
HINTERNET WebSocketHandle(VOID);
class=class="str">"cmt">// Connect to a server
class=class="str">"cmt">/*
  hsession: HINTERNET session handle
  host: is the url
  port: prefered port number to use
  secure: class="num">0 is false, non-zero is true
  class="kw">return: DWORD error code, class="num">0 indicates success
  and non-zero for failure
*/
DWORD Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure);

class=class="str">"cmt">// Send data to the WebSocket server
class=class="str">"cmt">/*
   bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
   pBuffer: pointer to the data to be sent
   dwLength: size of pBuffer data
   class="kw">return: DWORD error code, class="num">0 indicates success
   and non-zero for failure
*/
DWORD Send(WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType, class="type">void* pBuffer, DWORD dwLength);
class=class="str">"cmt">// Close the connection to the server
class=class="str">"cmt">/*
   status: WINHTTP_WEB_SOCKET_CLOSE_STATUS enumeration of the close notification to be sent
   reason: character class="type">class="kw">string of extra data sent with the close notification
   class="kw">return: DWORD error code, class="num">0 indicates success
   and non-zero for failure
*/

「收尾这组 WebSocket 接口怎么用」

上面这批方法覆盖了 MT5 里 WebSocket 客户端在连接建立后的收口动作:主动关闭、查服务端关闭原因、收数据、看状态、捞内部队列缓存。 Close() 接受 WINHTTP_WEB_SOCKET_CLOSE_STATUS 状态码和可选原因串,用来主动向服务端发关闭帧;reason 默认是 NULL,不传就只发状态码。QueryCloseStatus() 则是反向操作——服务端关连接时,你用 pusStatus 接状态码、pvReason 接原因文本,当 pvReason 为 NULL 且 dwReasonLength 为 0 时,pdwReasonLengthConsumed 会回写你该分配多大缓冲区。 Receive() 从服务端读帧,pBuffer 和 pLength 是你的缓冲区和长度,bytesRead 回写实际读到的字节数,pBufferType 区分文本或二进制帧;返回 0 是成功,非 0 即出错。Status() 直接回 ENUM_WEBSOCKET_STATE 枚举,用来在循环里判断客户端还活不活着。 Read() 和 ReadAvailable() 配合内部队列:Read() 把缓存帧拷进你的 pBuffer,ReadAvailable() 只回最近一帧的字节数,不拷贝。LastError() 与 SetError() 管错误码,EnableCallBack() 打开异步回调。 在 MT5 里跑这套,建议先 Status() 确认连接态再 Receive(),否则在已关闭套接字上调用可能返回非 0 错误码;外汇与贵金属行情推送走 WebSocket 时延迟和断连风险高,任何读取都该按错误码分支处理。

MQL5 / C++
DWORD Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS status, CHAR* reason = NULL);
class=class="str">"cmt">// Retrieve the close status sent by a server
class=class="str">"cmt">/*
 pusStatus: pointer to a close status code that will be filled upon class="kw">return.
 pvReason: pointer to a buffer that will receive a close reason
 dwReasonLength: The length of the pvReason buffer,
 pdwReasonLengthConsumed:The number of bytes consumed. If pvReason is NULL and dwReasonLength is class="num">0,
pdwReasonLengthConsumed will contain the size of the buffer that needs to be allocated
 by the calling application.
 class="kw">return: DWORD error code, class="num">0 indicates success
 and non-zero for failure
*/
DWORD QueryCloseStatus(USHORT* pusStatus, PVOID pvReason, DWORD dwReasonLength, DWORD* pdwReasonLengthConsumed);
class=class="str">"cmt">// read from the server
class=class="str">"cmt">/*
 bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
 pBuffer: pointer to the data to be sent
 pLength: size of pBuffer
 bytesRead: pointer to number bytes read from the server
 pBufferType: pointer to type of frame sent from the server
 class="kw">return: DWORD error code, class="num">0 indicates success
 and non-zero for failure
*/
DWORD Receive(PVOID pBuffer, DWORD pLength, DWORD* bytesRead, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);
class=class="str">"cmt">// Check client state
class=class="str">"cmt">/*
 class="kw">return: ENUM_WEBSOCKET_STATE enumeration
*/
ENUM_WEBSOCKET_STATE Status(VOID);
class=class="str">"cmt">// get frames cached in the internal queue
class=class="str">"cmt">/*
 pBuffer: User supplied container to which data is written to
 pLength: size of pBuffer
 pBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type
*/
VOID Read(BYTE* pBuffer, DWORD pLength, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);
class=class="str">"cmt">// get bytes received
class=class="str">"cmt">/*
 class="kw">return: Size of most recently cached frame sent from a server
*/
DWORD ReadAvailable(VOID);
class=class="str">"cmt">// get the last error
class=class="str">"cmt">/*
 class="kw">return: returns the last error code
*/
DWORD LastError(VOID);
class=class="str">"cmt">// activate callback function
class=class="str">"cmt">/*
 class="kw">return: DWORD error code, class="num">0 indicates success
 and non-zero for failure
*/
DWORD EnableCallBack(VOID);
class=class="str">"cmt">// set error 
class=class="str">"cmt">/*
 message: Error description to be captured
 errorcode: new user defined error code
*/
VOID SetError(const DWORD errorcode);
class=class="str">"cmt">// get the last completed operation
class=class="str">"cmt">/*
returns: DWORD constant of last websocket operation
*/

常见问题

同步调用会阻塞主线程,导致图表卡顿和响应延迟;改用Windows API的异步WinHTTP通道可让网络收发在后台跑,界面不卡。
一是MT5不直接暴露WinHTTP库需自行封装C++ DLL桥接,二是异步回调里不能碰MT5终端对象必须靠队列回传数据。
可以,小布盯盘的AIGC能接管异步入站数据做清洗和异动提示,你只需在品种页打开对应开关即可。
至少要有连接句柄、发送缓冲、接收队列和回调钩子;这样才能支撑连接、发消息、收消息和断线重连。
通过封装的DLL暴露Connect/Send/Recv函数,交易脚本周期轮询接收队列拿数据并转成图表或报警即可。