用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接·进阶篇
🔌

用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接·进阶篇

(2/3)· 同步WebSocket在MT5里卡死行情线程的坑,靠WinHTTP异步模式加自定义DLL才填得上

偏理论 第 2/3 篇
不少人在MT5里直接套用同步WebSocket,结果行情回调被网络等待拖死,EA间歇性假死。把WinHTTP当阻塞调用用,等于拿主线程去赌延迟。异步化不是可选项,是实盘稳定性的底线。

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 里复位状态,可能让重连逻辑死锁。

MQL5 / C++
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 表示关闭完成。外汇与贵金属行情通过这套异步通道转发时波动剧烈,实盘前请在模拟盘验证连接稳定性与断线重连逻辑。

MQL5 / C++
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。

MQL5 / C++
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 解析是否符合预期。

MQL5 / C++
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 定位。

MQL5 / C++
  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 用模拟账户跑满一次重连周期。

MQL5 / C++
  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;
  }
把连接状态交给小布盯盘
这些DLL握手与掉线诊断,小布盯盘的AIGC已内置,打开对应品种页即可看到连接健康度,你只管策略逻辑。

常见问题

不强制。会话句柄创建后、WebSocket连接初始化前或期间的任意阶段,都能对有效HINTERNET句柄调用WinHttpSetStatusCallback。
可以,小布盯盘品种页内置了连接层诊断,异步DLL上报的状态变化会被聚合成可读的健康度提示,省去你自己写日志解析。
实测中通过该选项可靠取回注册内容值多次失败,虽不排除实现错误,但为稳妥改用全局变量,细节在DLL实现节展开。
因MT5程序本质单线程执行环境,对此限制可适度放宽,不必像通用Windows服务那样做完整加锁。