用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接·进阶篇
(2/3)· 同步WebSocket在MT5里卡死行情线程的坑,靠WinHTTP异步模式加自定义DLL才填得上
WebSocket 客户端的回调与状态骨架
在 MT5 里做异步行情通道,先得把 WebSocket 客户端的接口轮廓理清。下面这段声明定义了会话收尾、错误与读写完成等回调,以及供回调访问的客户端容器,是后续写价格行为抓取模块的地基。 DWORD LastOperation(VOID); // 取最近一次完成的异步操作标识 VOID Free(VOID); // 释放会话句柄与相关资源 VOID OnError(const WINHTTP_ASYNC_RESULT* result); // 出错时回调,result 封装触发错误的事件 VOID OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype); // 读完成,read 为成功读取字节数 VOID OnClose(VOID); // 成功关闭 WebSocket 时回调 VOID OnSendComplete(const DWORD sent); // 异步发送完成,sent 为已发字节数 VOID OnCallBack(const DWORD operation); // 标记回调认定的已完成操作 客户端状态用枚举锁死六种可能:CLOSED=0、CLOSING=1、CONNECTING=2、CONNECTED=3、SENDING=4、POLLING=5。连不上时状态停在 CONNECTING,连上了才进 CONNECTED,发数据期间跳 SENDING——这些常量直接决定你 EA 里要不要重连或暂停发单。 extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>> clients; 这句把句柄到客户端对象的映射暴露给回调,意味着你在回调里拿 HINTERNET 就能查到对应客户端实例。 对外 API 三个就够用:client_reset 注销句柄;client_connect 建连,入参含 url、port、secure(非0走加密),出参 websocket_handle,返回 0 即成功;client_disconnect 销毁由 connect 建出的句柄。外汇与贵金属杠杆高、滑点突发行情多,异步断线若没在 OnError 里复位状态,可能让重连逻辑死锁。
DWORD LastOperation(VOID); class=class="str">"cmt">//deinitialize the session handle and free up resources VOID Free(VOID); class=class="str">"cmt">//the following methods define handlers meant to be triggered by the callback function// class=class="str">"cmt">// on error class=class="str">"cmt">/* result: pointer to WINHTTP_ASYNC_RESULT structure that encapsulates the specific event that triggered the error */ VOID OnError(const WINHTTP_ASYNC_RESULT* result); class=class="str">"cmt">// read completion handler class=class="str">"cmt">/* read: Number of bytes of data successfully read from the server buffertype: type of frame read-in. Called when successfull read is completed */ VOID OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype); class=class="str">"cmt">// websocket close handler class=class="str">"cmt">/* Handles the a successfull close request */ VOID OnClose(VOID); class=class="str">"cmt">// Send operation handler class=class="str">"cmt">/* sent: the number of bytes successfully sent to the server if any This is a handler for an asynchronous send that interacts with the callback function */ VOID OnSendComplete(const DWORD sent); class=class="str">"cmt">//set the last completed websocket operation class=class="str">"cmt">/* operation : constant defining the operation flagged as completed by callback function */ VOID OnCallBack(const DWORD operation); }; class="kw">struct Frame { std::vector<BYTE>frame_buffer; WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type; DWORD frame_size; }; 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">// container for websocket objects accessible to callback function class="kw">extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients; class=class="str">"cmt">// deinitializes a session handle class=class="str">"cmt">/* websocket_handle: HINTERNET the websocket handle to close */ VOID WEBSOCK_API client_reset(HINTERNET websocket_handle); class=class="str">"cmt">//creates a client connection to a server class=class="str">"cmt">/* url: the url of the server port: port secure: use secure connection(non-zero) or not(zero) websocket_handle: in-out,HINTERNET non NULL session handle class="kw">return: returns DWORD, zero if successful or non-zero on failure */ DWORD WEBSOCK_API client_connect(const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocket_handle); class=class="str">"cmt">//destroys a client connection to a server class=class="str">"cmt">/* websocket_handle: a valid(non NULL) websocket handle created by calling client_connect() */ class="type">void WEBSOCK_API client_disconnect(HINTERNET websocket_handle);
「WebSocket 客户端收发与状态接口拆解」
在 MT5 里用 WinHTTP 封装的 WebSocket 客户端,核心就是几个非阻塞调用:client_send 把数据推到服务器,client_read 从内部缓存取服务器下发的内容,client_poll 则负责监听响应,三者返回值都是 DWORD 错误码,0 为成功、非 0 即失败。 client_readable 返回内部缓存中上一帧的字节数,你可以用它先探一下缓冲水位再决定读不读,避免 out 缓冲区溢出;client_status 给出 ENUM_WEBSOCKET_STATE 枚举的连接状态,client_lastcallback_notification 回传上一次回调的唯一状态常量。 错误排查靠 client_lasterror,它把错误描述写进 lasterror 容器,并通过 lasterrornum 引用回写错误码,调用前务必保证容器长度 length 够用。 回调入口 WebSocketCallback 里用 clients.find(hInternet) 定位连接对象,命中后转发 OnCallBack 并按 dwInternetStatus 分支处理,例如 WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE 表示关闭完成。外汇与贵金属行情通过这套异步通道转发时波动剧烈,实盘前请在模拟盘验证连接稳定性与断线重连逻辑。
DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length); DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype); DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle); DWORD WEBSOCK_API client_lasterror(HINTERNET websocket_handle); DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle); ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle); DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle); HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle); class="type">void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) { if (WinHttpWebSocketClient::clients.find(hInternet)!= WinHttpWebSocketClient::clients.end()) { WinHttpWebSocketClient::clients[hInternet]->OnCallBack(dwInternetStatus); class="kw">switch (dwInternetStatus) { case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
◍ 回调分发与连接入口的实现细节
WinHTTP 的异步回调里,按状态码分流是 WebSocket 客户端稳定收发的核心。CLOSE、WRITE_COMPLETE、READ_COMPLETE、REQUEST_ERROR 各自触发对象方法,未列出的状态走 default 直接 break,避免无效处理拖慢消息循环。 读完成回调 OnReadComplete 中,bytesRX 记录本次接收字节数,rxBufferType 保存帧类型,状态机置回 CONNECTED;随后把 rxBuffer 中前 read 字节拷入 frame 并压入 frames 队列,意味着一帧数据从内核缓冲转移到应用层链表,MT5 里跑这段代码时可在 frames->size() 上打点看吞吐。 对外暴露的 client_connect 用 shared_ptr 管理 WebSocketClient 生命周期,Connect 失败即取 LastError 填 errorCode;成功则尝试 EnableCallBack,若回调注册失败会主动 Close 并回写错误码,调用方拿 websocketp_handle 前应先判 errorCode 是否为 0。
VOID WebSocketClient::OnCallBack(const DWORD operation) { completed_websocket_operation = operation; } VOID WebSocketClient::OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype) { bytesRX = read; rxBufferType = buffertype; status = ENUM_WEBSOCKET_STATE::CONNECTED; Frame frame; frame.frame_buffer.insert(frame.frame_buffer.begin(), rxBuffer.data(), rxBuffer.data() + read); frame.frame_type = buffertype; frame.frame_size = read; frames->push(frame); } VOID WebSocketClient::OnSendComplete(const DWORD sent) { bytesTX = sent; status = ENUM_WEBSOCKET_STATE::CONNECTED; class="kw">return; } VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result) { SetError(result->dwError); Reset(false); } DWORD WEBSOCK_API client_connect( const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocketp_handle) { DWORD errorCode = class="num">0; auto client = std::make_shared<WebSocketClient>(); if (client->Connect(url, port, secure) != NO_ERROR) errorCode = client->LastError(); else { HINTERNET handle = client->WebSocketHandle(); if (client->EnableCallBack()) { errorCode = client->LastError(); client->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
连接前的 URL 拆解与内存护栏
WebSocketClient::Connect 在真正建链前先卡一道状态检查:若 status 不是 CLOSED,会先尝试 Close 并返回 WEBSOCKET_ERROR_CLOSING_ACTIVE_CONNECTION,避免在上一个活跃连接没清干净时叠加新连接。 若 hSession 为空才走 Initialize(),初始化失败直接 Reset(false) 并回传错误码,不会继续往下分配资源。 scheme、hostName、urlPath 三块缓冲分别用 0x20、0x100、0x1000 宽字符长度做 unique_ptr 托管;任一 new 失败或 wcsncpy_s 拷贝返回非 0,都会走 ERROR_NOT_ENOUGH_MEMORY 或 GetLastError() 并 Reset,等于给 URL 拆解上了内存护栏。 UrlComponents 先用 memset 清零,再把 dwSchemeLength、dwHostNameLength 等六个长度字段统一置为 -1,告诉 WinHttpCrackUrl 由系统回填实际长度,随后用拆解出的 lpszScheme / lpszHostName / lpszUrlPath 分别拷进独立缓冲。开 MT5 把这段塞进你自己的 WebSocket 封装类,断点看 UrlComponents 各长度字段被回填成多少,就能确认 URL 解析是否符合预期。
DWORD WebSocketClient::Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure) { if((status != ENUM_WEBSOCKET_STATE::CLOSED)) { ErrorCode = Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,NULL); class="kw">return WEBSOCKET_ERROR_CLOSING_ACTIVE_CONNECTION; } status = ENUM_WEBSOCKET_STATE::CONNECTING; class=class="str">"cmt">// Return class="num">0 for success if(hSession == NULL) { ErrorCode = Initialize(); if(ErrorCode) { Reset(false); class="kw">return ErrorCode; } } class=class="str">"cmt">// Cracked URL variable pointers URL_COMPONENTS UrlComponents; class=class="str">"cmt">// Create cracked URL buffer variables std::unique_ptr <WCHAR> scheme(new WCHAR[0x20]); std::unique_ptr <WCHAR> hostName(new WCHAR[0x100]); std::unique_ptr <WCHAR> urlPath(new WCHAR[0x1000]); DWORD dwFlags = class="num">0; if(secure) dwFlags |= WINHTTP_FLAG_SECURE; if(scheme == NULL || hostName == NULL || urlPath == NULL) { ErrorCode = ERROR_NOT_ENOUGH_MEMORY; Reset(); class="kw">return ErrorCode; } class=class="str">"cmt">// Clear error&class="macro">#x27;s ErrorCode = class="num">0; class=class="str">"cmt">// Setup UrlComponents structure memset(&UrlComponents, class="num">0, class="kw">sizeof(URL_COMPONENTS)); UrlComponents.dwStructSize = class="kw">sizeof(URL_COMPONENTS); UrlComponents.dwSchemeLength = -class="num">1; UrlComponents.dwHostNameLength = -class="num">1; UrlComponents.dwUserNameLength = -class="num">1; UrlComponents.dwPasswordLength = -class="num">1; UrlComponents.dwUrlPathLength = -class="num">1; UrlComponents.dwExtraInfoLength = -class="num">1; class=class="str">"cmt">// Get the individual parts of the url if(!WinHttpCrackUrl(host, NULL, class="num">0, &UrlComponents)) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(); class="kw">return ErrorCode; } class=class="str">"cmt">// Copy cracked URL hostName & UrlPath to buffers so they are separated if(wcsncpy_s(scheme.get(), 0x20, UrlComponents.lpszScheme, UrlComponents.dwSchemeLength) != class="num">0 || wcsncpy_s(hostName.get(), 0x100, UrlComponents.lpszHostName, UrlComponents.dwHostNameLength) != class="num">0 || wcsncpy_s(urlPath.get(), 0x1000, UrlComponents.lpszUrlPath, UrlComponents.dwUrlPathLength) != class="num">0) { ErrorCode = GetLastError(); Reset(false);
「端口推断与 WinHTTP 握手流程」
当调用方未显式传入端口(port 为 0)时,代码依据 URL scheme 自动补全:wss/https 映射到 INTERNET_DEFAULT_HTTPS_PORT(443),ws/http 映射到 INTERNET_DEFAULT_HTTP_PORT(80);若 scheme 不匹配这两者,直接返回 ERROR_INVALID_PARAMETER 并 Reset(false),连接根本不会发起。 拿到 host 与端口后,WinHttpConnect 建立会话句柄,失败则用 GetLastError() 取值并回退。随后 WinHttpOpenRequest 以 GET 方法构造请求,并连续两次 WinHttpSetOption:先置空客户端证书上下文,再打上 WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET 标记——这正是把一条普通 HTTP 通道升级成 WebSocket 的关键开关。 最后 WinHttpSendRequest 发出升级请求,WinHttpReceiveResponse 阻塞等服务器回执;任一步返回 false 都会把 ErrorCode 设为系统错误码并 Reset(false) 退出。在 MT5 里跑这套逻辑时,若 443 端口被经纪商环境限制,wss 连接大概率在 WinHttpConnect 阶段就折了,可先打印 ErrorCode 定位。
class="kw">return ErrorCode; } if(port == class="num">0) { if((_wcsicmp(scheme.get(), L"wss") == class="num">0) || (_wcsicmp(scheme.get(), L"https") == class="num">0)) { UrlComponents.nPort = INTERNET_DEFAULT_HTTPS_PORT; } else if((_wcsicmp(scheme.get(), L"ws") == class="num">0) || (_wcsicmp(scheme.get(), L"http")) == class="num">0) { UrlComponents.nPort = INTERNET_DEFAULT_HTTP_PORT; } else { ErrorCode = ERROR_INVALID_PARAMETER; Reset(false); class="kw">return ErrorCode; } } else UrlComponents.nPort = port; class=class="str">"cmt">// Call the WinHttp Connect method hConnect = WinHttpConnect(hSession, hostName.get(), UrlComponents.nPort, class="num">0); if(!hConnect) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; } class=class="str">"cmt">// Create a HTTP request hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.get(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags); if(!hRequest) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; } class=class="str">"cmt">// Set option for client certificate if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, class="num">0)) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; } class=class="str">"cmt">// Add WebSocket upgrade to our HTTP request class="macro">#pragma prefast(suppress:class="num">6387, "WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET does not take any arguments.") if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, class="num">0, class="num">0)) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; } class=class="str">"cmt">// Send the WebSocket upgrade request. if(!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, class="num">0, class="num">0, class="num">0, class="num">0, class="num">0)) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; } class=class="str">"cmt">// Receive response from the server if(!WinHttpReceiveResponse(hRequest, class="num">0)) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; }
◍ 收尾的连接升级与对外 API 封装
WebSocket 握手走到最后一步,靠 WinHttpWebSocketCompleteUpgrade 把普通 HTTP 请求句柄升级成双工通道。若返回 0,说明升级失败,此时取 GetLastError 后调用 Reset(false) 清理,并把错误码交回调用方;成功则把内部状态置为 CONNECTED,函数最终应返回 0。 EnableCallBack 负责把对象指针塞进上下文并挂上全局回调。WinHttpSetOption 用 WINHTTP_OPTION_CONTEXT_VALUE 绑定 this 指针,随后 WinHttpSetStatusCallback 以 WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS 监听所有完成事件;任一步返回无效就透传错误码,不影响主流程继续。 对外暴露的 client_send、client_poll、client_read 等 API 全部以 HINTERNET 句柄做索引,先在 clients 映射里查合法性,空句柄或查不到统一回 WEBSOCKET_ERROR_INVALID_HANDLE。client_poll 内部直接调 Receive 往预分配 rxBuffer 塞数据,长度取 rxBuffer.size(),实际收字节数写进 bytesRX——这套封装让 EA 侧不必关心 C++ 对象生命周期。 client_readable 返回可读字节数,EA 轮询它再决定要不要 client_read,能压低无谓的拷贝开销。外汇与贵金属行情走这种实时通道时波动快、滑点风险高,句柄非法或回调丢失都可能让报价断流,上线前建议在 MT5 用模拟账户跑满一次重连周期。
class=class="str">"cmt">// Finally complete the upgrade hWebSocket = WinHttpWebSocketCompleteUpgrade(hRequest, NULL); if(hWebSocket == class="num">0) { class=class="str">"cmt">// Handle error ErrorCode = GetLastError(); Reset(false); class="kw">return ErrorCode; } status = ENUM_WEBSOCKET_STATE::CONNECTED; class=class="str">"cmt">// Return should be zero class="kw">return ErrorCode; } DWORD WebSocketClient::EnableCallBack(VOID) { 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="kw">return ErrorCode; } DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length) { 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]->Send(buffertype, message, length); class="kw">return out; } DWORD WEBSOCK_API client_lastcallback_notification(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]->LastOperation(); class="kw">return out; } DWORD WEBSOCK_API client_poll(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]->Receive(clients[websocket_handle]->rxBuffer.data(), (DWORD)clients[websocket_handle]->rxBuffer.size(), &clients[websocket_handle]->bytesRX, &clients[websocket_handle]->rxBufferType); class="kw">return out; } ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle) { ENUM_WEBSOCKET_STATE out = {}; if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end()) out = {}; else out = clients[websocket_handle]->Status(); class="kw">return out; } DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype) { DWORD rout = class="num">0; if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end()) rout = WEBSOCKET_ERROR_INVALID_HANDLE; else clients[websocket_handle]->Read(out, out_size, buffertype); class="kw">return rout; } DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle) { DWORD out = class="num">0; if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end()) out = class="num">0; else out = clients[websocket_handle]->ReadAvailable(); class="kw">return out; }