English Русский Deutsch 日本語
preview
用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接

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

MetaTrader 5示例 |
97 9
Francis Dube
Francis Dube

概述

在文章《MetaTrader 5中的WebSocket:使用Windows API》中,阐述了如何在MetaTrader 5应用程序中利用Windows API实现WebSocket客户端。但其中所展示的实现方式受限于其同步运行模式。

在本文中,我们将再次探讨如何使用Windows API为MetaTrader 5程序构建WebSocket客户端,目标是实现异步客户端功能。实现这一目标的一种实用方法是创建一个自定义动态链接库(DLL),该库导出适合与MetaTrader 5应用程序集成的函数。

因此,本文将讨论该DLL的开发过程,并通过一个MetaTrader 5程序示例展示其应用。


WinHTTP异步模式

根据WinHTTP库文档中的说明,要在WinHTTP库中实现异步操作,需满足两个前提条件。首先,在调用WinHTTPOpen函数时,会话句柄必须配置为WINHTTP_FLAG_ASYNC或WINHTTP_FLAG_SECURE_DEFAULTS标识。

// 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;
// Return error code
return ErrorCode;

在成功创建有效的会话句柄后,用户需注册一个回调函数,以接收与特定WinHTTP函数调用相关的各类事件通知。此状态回调函数会通过通知标识获知异步操作的进度情况。

回调函数的注册通过WinHttpSetStatusCallback函数完成,该函数还允许指定回调函数将处理的通知标识。用户可以选择订阅全套通知,也可以仅订阅部分通知。此外,还可以分别为会话句柄、请求句柄和WebSocket句柄指定不同的回调函数。

需要注意的是,在创建会话句柄后立即注册回调函数并非强制要求;在WebSocket连接初始化之前或进行期间的任何阶段,均可针对任何有效的HINTERNET句柄调用WinHttpSetStatusCallback函数。

if (!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this)))
{
        // Handle error
        ErrorCode = GetLastError();
        return ErrorCode;
}

if (WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
{
        ErrorCode = GetLastError();
        return ErrorCode;
}

用户自定义回调函数的签名中包含一个指向用户自定义数据结构的参数,该结构通常被称为内容值。这一机制便于从回调函数中传递数据。

内容值必须在注册回调函数之前,通过调用WinHttpSetOption函数并设置WINHTTP_OPTION_CONTEXT_VALUE选项标识来指定。需要注意的是,在实际测试中,通过这种方法可靠地检索已注册的内容值存在困难。

尽管不能完全排除实现错误的可能性,但鉴于多次尝试均告失败,我们不得不采用全局变量作为替代方案,这一细节将在后续讨论DLL实现时详细阐述。

最后,关于用户自定义回调函数,一个关键考量因素是线程安全性的要求。然而,鉴于当前工作是为MetaTrader 5环境创建一个DLL,这一限制可适当放宽。这是因为MetaTrader 5程序本质上通常是单线程的,虽然DLL代码在加载进程的线程池中执行,但在MetaTrader 5程序中,只有一个线程处于活动状态。

void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)


DLL的实现

该DLL在Visual Studio环境中使用C++编程语言构建。这一过程要求在Visual Studio中安装“C++桌面开发”工作负载,以及Windows 10或Windows 11软件开发工具包(SDK)。Windows SDK是必需的,因为它提供了WinHTTP库文件(.lib),DLL在编译时将链接到该文件。生成的DLL至少包含三个基本组件。

第一个是一个封装了WinHTTP WebSocket客户端核心功能的类。第二个是一个单独的回调函数,该函数与一个全局变量协同工作,便于在回调函数的作用域内及外部对WebSocket连接进行操作。第三个组件是一组简化的函数包装器,这些包装器将由DLL导出,供MetaTrader 5程序使用。实现过程从asyncwebsocketclient.h头文件中定义的代码开始。

该头文件首先声明了WebSocketClient类,其中每个实例代表一个独立的客户端连接。

// WebSocket client class
class WebSocketClient {
private:
        // Application session handle to use with this connection
        HINTERNET hSession;
        // Windows connect handle
        HINTERNET hConnect;
        // The initial HTTP request handle to start the WebSocket handshake
        HINTERNET hRequest;
        // Windows WebSocket handle
        HINTERNET hWebSocket;
        //initialization flag
        DWORD initialized;
        //sent bytes
        DWORD bytesTX;
        //last error code
        DWORD ErrorCode;
        //last completed websocket operation as indicated by callback function
        DWORD completed_websocket_operation;
        //internal queue of frames sent from a server
        std::queue<Frame>* frames;
        //client state;
        ENUM_WEBSOCKET_STATE status;
        //sets an hSession handle
        DWORD Initialize(VOID);
        // reset state of object
        /*
         reset_error: boolean flag indicating whether to rest the
         internal error buffers.
        */
        VOID  Reset(bool reset_error = true);
        
public:
        //constructor(s)
        WebSocketClient(VOID);
        WebSocketClient(const WebSocketClient&) = delete;
        WebSocketClient(WebSocketClient&&) = delete;
        WebSocketClient& operator=(const WebSocketClient&) = delete;
        WebSocketClient& operator=(WebSocketClient&&) = delete;
        //destructor
        ~WebSocketClient(VOID);
        //received bytes;
        DWORD bytesRX;
        // receive buffer
        std::vector<BYTE> rxBuffer;
        // received frame type;
        WINHTTP_WEB_SOCKET_BUFFER_TYPE rxBufferType;
        // Get the winhttp websocket handle
        /*
        return: returns the hWebSocket handle which is used to 
        identify a websocket connection instance
        */
        HINTERNET WebSocketHandle(VOID);

        // Connect to a server
        /* 
           hsession: HINTERNET session handle
           host: is the url
           port: prefered port number to use
           secure: 0 is false, non-zero is true
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure);
        
        // Send data to the WebSocket server
        /*
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           pBuffer: pointer to the data to be sent
           dwLength: size of pBuffer data
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Send(WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType, void* pBuffer, DWORD dwLength);

        // Close the connection to the server
        /*
           status: WINHTTP_WEB_SOCKET_CLOSE_STATUS enumeration of the close notification to be sent
           reason: character string of extra data sent with the close notification
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS status, CHAR* reason = NULL);

        // Retrieve the close status sent by a server
        /*
           pusStatus: pointer to a close status code that will be filled upon 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 0,
       pdwReasonLengthConsumed will contain the size of the buffer that needs to be allocated
       by the calling application.
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD QueryCloseStatus(USHORT* pusStatus, PVOID pvReason, DWORD dwReasonLength, DWORD* pdwReasonLengthConsumed);

        // read from the server
        /*
           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
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Receive(PVOID pBuffer, DWORD pLength, DWORD* bytesRead, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);

        // Check client state
        /*
           return: ENUM_WEBSOCKET_STATE enumeration
        */
        ENUM_WEBSOCKET_STATE Status(VOID);

        // get frames cached in the internal queue
        /*
           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);

        // get bytes received
        /*
           return: Size of most recently cached frame sent from a server
        */
        DWORD ReadAvailable(VOID);

        // get the last error
        /*
           return: returns the last error code
        */
        DWORD LastError(VOID);

        // activate callback function
        /*
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD EnableCallBack(VOID);
        // set error 
        /*
           message: Error description to be captured
           errorcode: new user defined error code
        */
        VOID SetError(const DWORD errorcode);
         
        // get the last completed operation
        /*
          returns: DWORD constant of last websocket operation
        */
        DWORD LastOperation(VOID);

        //deinitialize the session handle and free up resources
        VOID Free(VOID);

        //the following methods define handlers meant to be triggered by the callback function//
        
        // on error 
        /*
           result: pointer to WINHTTP_ASYNC_RESULT structure that 
           encapsulates the specific event that triggered the error
        */
        VOID OnError(const WINHTTP_ASYNC_RESULT* result);

        // read completion handler
        /*
           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);

        // websocket close handler
        /*
           Handles the a successfull close request
        */
        VOID OnClose(VOID);

        // Send operation handler
        /*
           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);

        //set the last completed websocket operation 
        /*
          operation : constant defining the operation flagged as completed by callback function
        */
        VOID OnCallBack(const DWORD operation);
};

除了该类之外,还定义了结构体Frame,用于表示消息帧。

struct Frame
{
        std::vector<BYTE>frame_buffer;
        WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type;
        DWORD frame_size;
};

此外,还声明了枚举类型ENUM_WEBSOCKET_STATE,用于描述WebSocket连接的各种状态。

// client state
enum ENUM_WEBSOCKET_STATE
{
        CLOSED = 0,
        CLOSING = 1,
        CONNECTING = 2,
        CONNECTED = 3,
        SENDING = 4,
        POLLING = 5
};

接下来,asyncwebsocketclient.h声明了一个名为clients的全局变量。该变量是一个容器(具体来说是一个映射表),用于存储活跃的WebSocket连接。这个映射表容器的全局作用域确保了库内定义的任何回调函数均可访问它。

// container for  websocket objects accessible to callback function
extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;

asyncwebsocketclient.h文件结尾处定义了一组带有WEBSOCK_API限定符的函数。该限定符用于标记这些函数,以便由DLL导出。这些函数即为前文提到的函数包装器,构成了开发者在其MetaTrader 5应用程序中与DLL进行交互的接口。

// deinitializes a session handle
/*
   websocket_handle: HINTERNET the websocket handle to close
*/
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle);

//creates a client connection to a server
/*   
           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
           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);

//destroys a client connection to a server
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
*/

void WEBSOCK_API client_disconnect(HINTERNET websocket_handle);

//writes data to a server (non blocking)
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           message: pointer to the data to be sent
           length: size of pBuffer data
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length);

//reads data sent from a server cached internally
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           out: User supplied container to which data is written to
           out_size: size of out buffer
           buffertype: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type 
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype);

//listens for a response from a server (non blocking)
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle);

//gets the last generated error
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           lasterror: container that will hold the error description
           length: the size of lasterror container
           lasterrornum: reference to which the last error code is written
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API  client_lasterror(HINTERNET websocket_handle);

//checks whether there is any data cached internally
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: returns the size of the last received frame in bytes;
*/

DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle);

//return the state of a websocket connection
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: ENUM_WEBSOCKET_STATE enumeration of the state of a client
*/

ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle);

//return the last websocket operation
/*
     websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
     return : DWORD constant corresponding to a unique callback status value as defined in API    
*/
DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle);

//return the websocket handle
/*
     websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
     return : HINTERNET returns the websocket handle for a client connection 
*/
HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle);

对WebSocketCallback()函数定义的检查表明,该函数利用全局变量clients来管理通知。当前实现配置为处理WinHTTP文档中所述的“完成通知”。这些通知会在任何异步操作成功完成时触发。例如,WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE表示发送操作已完成,而WINHTTP_CALLBACK_STATUS_READ_COMPLETE则表示读取操作已完成。

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);
                switch (dwInternetStatus)
                {
                case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnClose();
                        break;

                case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnSendComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred);
                        break;

                case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnReadComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred, ((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->eBufferType);
                        break;

                case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
                        WinHttpWebSocketClient::clients[hInternet]->OnError((WINHTTP_ASYNC_RESULT*)lpvStatusInformation);
                        break;

                default:
                        break;
                }
        }

}

回调函数的hInternet参数是最初注册回调时所使用的HINTERNET句柄。该句柄用于索引全局映射容器clients中的成员,从而获取指向WebSocketClient实例的指针。具体的回调通知通过dwInternetStatus参数传递。值得注意的是,lpvStatusInformation和dwStatusInformationLength参数所表示的数据会根据dwInternetStatus的值而变化。

例如,当dwInternetStatus取值为WINHTTP_CALLBACK_STATUS_READ_COMPLETE或WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE时,lpvStatusInformation参数包含一个指向WINHTTP_WEB_SOCKET_STATUS结构的指针,而dwStatusInformationLength则指示所引用数据的大小。

我们的实现有选择地处理此回调函数提供的一部分通知,每次处理都会修改相应WebSocketClient实例的状态。具体而言,OnCallBack()方法捕获与这些通知相关的状态码。这些信息保存在WebSocketClient实例中,可通过包装函数向用户公开。

VOID WebSocketClient::OnCallBack(const DWORD operation)
{
        completed_websocket_operation = operation;
}

WebSocketClient类中的OnReadComplete()方法负责将原始数据传输至帧的队列缓冲区中,用户随后可查询该缓冲区中是否有可供提取的数据。

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);
}

OnSendComplete()方法会更新内部字段以标记发送操作成功,同时还会触发WebSocket客户端状态的变更。

VOID WebSocketClient::OnSendComplete(const DWORD sent)
{
        bytesTX = sent;
        status = ENUM_WEBSOCKET_STATE::CONNECTED;
        return;
}

最后,OnError()方法会捕获通过lpvStatusInformation参数传递的任何与错误相关的信息。

VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result)
{
        SetError(result->dwError);
        Reset(false);
}

通过client_connect()函数可实现与服务器建立连接。调用该函数时需传入服务器地址(字符串形式)、端口号(整数形式)、一个布尔值(用于指定连接是否应为安全连接)以及一个指向HINTERNET值的指针。该函数会返回一个DWORD值,同时设置HINTERNET参数。如果在连接过程中出现任何错误,该函数会将HINTERNET参数设置为NULL,并返回一个非零错误代码。

在内部实现上,client_connect()函数会初始化一个WebSocketClient类的实例,并利用该实例建立连接。成功建立连接后,指向该WebSocketClient实例的指针会被存储在全局clients容器中,以该实例的WebSocket句柄作为唯一键值。该WebSocket句柄既用于在DLL内部操作中唯一标识特定的WebSocket连接,也供外部调用的MetaTrader 5程序使用以唯一标识该连接。

DWORD  WEBSOCK_API client_connect( const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocketp_handle)
{
        DWORD errorCode = 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);
                        client->Free();
                        handle = NULL;
                }
                else
                {
                        clients[handle] = client;
                        *websocketp_handle = handle;
                }
        }
        return errorCode;
}

WebSocket连接的初始化与建立由WebSocketClient类的Connect()方法负责管理。连接过程最初以同步方式执行。随后,通过调用EnableCallBack()方法在WebSocket句柄上注册回调函数,从而启用异步事件通知机制。

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);
      return WEBSOCKET_ERROR_CLOSING_ACTIVE_CONNECTION;
     }

   status = ENUM_WEBSOCKET_STATE::CONNECTING;


// Return 0 for success
   if(hSession == NULL)
     {
      ErrorCode = Initialize();
      if(ErrorCode)
        {
         Reset(false);
         return ErrorCode;
        }
     }

// Cracked URL variable pointers
   URL_COMPONENTS UrlComponents;

// 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 = 0;
   if(secure)
      dwFlags |= WINHTTP_FLAG_SECURE;

   if(scheme == NULL || hostName == NULL || urlPath == NULL)
     {
      ErrorCode = ERROR_NOT_ENOUGH_MEMORY;
      Reset();
      return ErrorCode;
     }

// Clear error's
   ErrorCode = 0;

// Setup UrlComponents structure
   memset(&UrlComponents, 0, sizeof(URL_COMPONENTS));
   UrlComponents.dwStructSize = sizeof(URL_COMPONENTS);
   UrlComponents.dwSchemeLength = -1;
   UrlComponents.dwHostNameLength = -1;
   UrlComponents.dwUserNameLength = -1;
   UrlComponents.dwPasswordLength = -1;
   UrlComponents.dwUrlPathLength = -1;
   UrlComponents.dwExtraInfoLength = -1;

// Get the individual parts of the url
   if(!WinHttpCrackUrl(host, NULL, 0, &UrlComponents))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset();
      return ErrorCode;
     }

// Copy cracked URL hostName & UrlPath to buffers so they are separated
   if(wcsncpy_s(scheme.get(), 0x20, UrlComponents.lpszScheme, UrlComponents.dwSchemeLength) != 0 ||
      wcsncpy_s(hostName.get(), 0x100, UrlComponents.lpszHostName, UrlComponents.dwHostNameLength) != 0 ||
      wcsncpy_s(urlPath.get(), 0x1000, UrlComponents.lpszUrlPath, UrlComponents.dwUrlPathLength) != 0)
     {
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

   if(port == 0)
     {
      if((_wcsicmp(scheme.get(), L"wss") == 0) || (_wcsicmp(scheme.get(), L"https") == 0))
        {
         UrlComponents.nPort = INTERNET_DEFAULT_HTTPS_PORT;
        }
      else
         if((_wcsicmp(scheme.get(), L"ws") == 0) || (_wcsicmp(scheme.get(), L"http")) == 0)
           {
            UrlComponents.nPort = INTERNET_DEFAULT_HTTP_PORT;
           }
         else
           {
            ErrorCode = ERROR_INVALID_PARAMETER;
            Reset(false);
            return ErrorCode;
           }
     }
   else
      UrlComponents.nPort = port;

// Call the WinHttp Connect method
   hConnect = WinHttpConnect(hSession, hostName.get(), UrlComponents.nPort, 0);
   if(!hConnect)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Create a HTTP request
   hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.get(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags);
   if(!hRequest)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Set option for client certificate

   if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Add WebSocket upgrade to our HTTP request
#pragma prefast(suppress:6387, "WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET does not take any arguments.")
   if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, 0, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Send the WebSocket upgrade request.
   if(!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, 0, 0, 0, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Receive response from the server
   if(!WinHttpReceiveResponse(hRequest, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Finally complete the upgrade
   hWebSocket = WinHttpWebSocketCompleteUpgrade(hRequest, NULL);
   if(hWebSocket == 0)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

   status = ENUM_WEBSOCKET_STATE::CONNECTED;

// Return should be zero
   return ErrorCode;
  }

DWORD WebSocketClient::EnableCallBack(VOID)
  {
   if(!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this)))
     {
      // Handle error
      ErrorCode = GetLastError();
      return ErrorCode;
     }

   if(WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
     {
      ErrorCode = GetLastError();
      return ErrorCode;
     }

   return ErrorCode;
  }

在与服务器建立连接并获取有效句柄后,用户即可启动与远程端点的通信。向服务器传输数据可通过调用client_send()函数实现。该函数需要以下参数:一个有效的WebSocket句柄、一个WINHTTP_WEB_SOCKET_BUFFER_TYPE枚举值(用于指定要传输的WebSocket帧类型)、一个包含数据负载的字节数组,以及一个表示数据数组大小的ulong参数。如果未遇到即时错误,该函数将返回0值;否则,将返回特定错误代码。

在内部实现中,WinHttpWebSocketSend()函数以异步方式调用。因此,client_send()函数的返回值仅代表中间状态,表明在发送操作设置过程中未出现初步错误。实际数据传输的结果不会以同步方式返回。相反,结果会通过已注册的回调函数以异步通知的形式传达。在发送操作成功的情况下,预期会收到WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE通知。反之,如果在任何操作(发送或接收)过程中发生错误,通常会向回调函数传递WINHTTP_CALLBACK_STATUS_REQUEST_ERROR通知。

DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length)
  {
   DWORD out = 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);

   return out;
  }

通过client_lastcallback_notification()函数可获取内部回调函数接收到的通知。该函数会返回针对特定连接(由作为唯一参数提供的WebSocket句柄标识)的回调函数最近接收到的通知。以下代码片段展示了在MetaTrader 5程序中处理这些通知的一种可能的方法。与这些通知对应的符号常量定义在asyncwinhttp.mqh文件中,这些常量源自原始的winhttp.h头文件。

DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastOperation();

   return out;
  }

接收服务器传输的数据,首先需通过调用client_poll()函数将客户端置于所谓的“轮询状态”。此操作会在WebSocketClient类内部调用WinHttpWebSocketReceive()函数。与发送操作类似,WinHTTP函数以异步方式调用,因此会立即返回一个中间状态。

WebSocketClient类包含内部缓冲区,用于在数据到达时存储原始数据。一旦读取操作成功完成,这些数据会被加入内部数据结构的队列中。这一过程由WebSocketClient类的OnReadComplete()方法管理。读取操作完成后,WebSocket连接的状态会发生变化,并停止主动“监听”传入消息。

这意味着异步读取请求并非持续进行,也不代表持续轮询。要获取服务器后续发送的消息,必须再次调用client_poll()函数。本质上讲,调用client_poll()会使WebSocket客户端进入临时非阻塞轮询状态,在数据可用时捕获数据,并随后触发WINHTTP_CALLBACK_STATUS_READ_COMPLETE通知。

可通过调用client_status()函数查询WebSocket客户端的当前状态,该函数返回一个ENUM_WEBSOCKET_STATE枚举类型的值。

DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle)
  {
   DWORD out = 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);

   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();

   return out;
  }

通过client_read()函数可以方便地获取从服务器接收到的原始数据,该函数接受以下参数:

  • 一个有效的HINTERNET类型的WebSocket句柄,
  • 一个已预先分配的字节数组的引用,以及一个指定上述数组大小的ulong值, 
  • 一个WINHTTP_WEB_SOCKET_BUFFER_TYPE类型值的引用。
接收到的数据会被写入提供的字节数组中,同时WebSocket帧的类型会被复制到buffertype参数中。其关键在于,该函数通过从已接收帧的内部队列中读取数据来操作,而非直接与网络套接字交互。因此,client_read()是一个同步操作,独立于WinHTTP库的异步机制。如果返回非0值,则表示从内部队列复制数据失败。使用此函数成功检索到一个帧后,该帧会从内部队列中移除(出队)。通过调用client_readable()函数,确定当前位于从服务器接收到的帧队列最前面的数据帧的大小。
DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype)
  {
   DWORD rout = 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);

   return rout;
  }

DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = 0;
   else
      out = clients[websocket_handle]->ReadAvailable();

   return out;
  }

调用client_lasterror()函数可获取错误代码。该函数返回最后一次遇到的错误对应的DWORD值。用户还可以通过client_websocket_handle()函数获取当前的WebSocket句柄值。在尝试确定某个句柄是否已关闭时,此功能会很有用。

DWORD WEBSOCK_API  client_lasterror(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastError();

   return out;
  }

HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle)
  {
   HINTERNET out = NULL;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = NULL;
   else
      out = clients[websocket_handle]->WebSocketHandle();
   return out;
  }

通过调用client_disconnect()函数可无缝地终止与服务器的连接。该函数不返回任何值。然而,它会立即将WebSocket客户端的状态切换为“正在关闭”状态。如果随后从服务器接收到对应的关闭帧,则会触发WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE通知,进而将WebSocket状态更改为“已关闭”。

void WEBSOCK_API client_disconnect(HINTERNET websocket_handle)
  {

   if(clients.find(websocket_handle) != clients.end())
     {
      if(clients[websocket_handle]->WebSocketHandle() != NULL)
        {
         clients[websocket_handle]->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
        }
     }

   return;
  }

该DLL导出的最后一个函数是client_reset()。理想情况下,应在与服务器断开连接后调用此函数。其作用是释放与已关闭连接相关的内部内存缓冲区。虽然并非严格强制要求调用此函数,但这样做有助于回收程序执行过程中其它地方可能需要的内存资源。调用client_reset()会使与指定WebSocket句柄相关的所有数据失效,包括错误代码、错误信息以及内部帧队列中剩余的任何未读数据。

VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
  {
   if(clients.find(websocket_handle) != clients.end())
     {
      clients[websocket_handle]->Free();
      clients.erase(websocket_handle);
     }
  }


重新定义CWebsocket类

在查看利用前文所述函数开发的MetaTrader 5应用程序之前,将对前述文章中讨论过的CWebsocket类进行重新定义。此次重新定义旨在使该类能够利用新开发的异步WebSocket客户端。此适配版本的源代码出自asyncwebsocket.mqh文件。

ENUM_WEBSOCKET_STATE枚举类型已扩展,新增了反映客户端异步特性的其他状态。当发起异步读取操作时,客户端进入POLLING(轮询)状态。当底层套接字接收到数据并使其可供检索时,回调函数会发出异步读取操作完成的信号,此时WebSocket客户端的状态会转换为其默认状态:CONNECTED(已连接)。类似地,异步发送操作会使状态变为SENDING(发送中)。该操作的结果会通过回调函数异步传达,如果传输成功,客户端状态将返回默认的CONNECTED状态。

//+------------------------------------------------------------------+
//|   client state enumeration                                       |
//+------------------------------------------------------------------+

// client state
enum ENUM_WEBSOCKET_STATE
  {
   CLOSED = 0,
   CLOSING = 1,
   CONNECTING = 2,
   CONNECTED = 3,
   SENDING = 4,
   POLLING = 5
  };

为适应增强后的功能,CWebsocket类中集成了多个新方法。其余方法保留了原有函数签名,仅内部实现经过修改以适配新的DLL依赖。这些方法具体如下:

Connect():此方法作为与服务器建立连接的初始交互点。它接受以下参数:

  •  _serveraddress:服务器的完整地址(字符串类型)。
  •  _port:服务器的端口号(无符号短整型)。
  •  _secure:布尔值,指示是否应建立安全连接(布尔类型)。

该方法的实现已大幅简化,因为大部分连接建立逻辑现由底层DLL处理。

//+------------------------------------------------------------------------------------------------------+
//|Connect method used to set server parameters and establish client connection                          |
//+------------------------------------------------------------------------------------------------------+
bool CWebsocket::Connect(const string _serveraddress,const INTERNET_PORT port=443, bool secure = true)
  {
   if(initialized)
     {
      if(StringCompare(_serveraddress,serveraddress,false))
         Close();
      else
         return(true);
     }

   serveraddress = _serveraddress;

   int dot=StringFind(serveraddress,".");

   int ss=(dot>0)?StringFind(serveraddress,"/",dot):-1;

   serverPath=(ss>0)?StringSubstr(serveraddress,ss+1):"/";

   int sss=StringFind(serveraddress,"://");

   if(sss<0)
      sss=-3;

   serverName=StringSubstr(serveraddress,sss+3,ss-(sss+3));
   serverPort=port;


   DWORD connect_error = client_connect(serveraddress,port,ulong(secure),hWebSocket);

   if(hWebSocket<=0)
     {
      Print(__FUNCTION__," Connection to ", serveraddress, " failed. \n", GetErrorDescription(connect_error));
      return(false);
     }
   else
      initialized = true;

   return(true);
  }

如果Connect()方法返回布尔值true,表明连接已成功建立,此时即可通过WebSocket客户端开始数据传输。为此提供了两种方法:

  •  SendString():此方法接受一个字符串作为输入参数。
  •  Send():此方法接受一个无符号字符数组作为唯一参数。

两个方法在成功启动发送操作后均返回布尔值true,且内部均调用私有方法client_send()(该私有方法负责处理该类的所有发送操作)。

//+------------------------------------------------------------------+
//|public method for sending raw string messages                     |
//+------------------------------------------------------------------+
bool CWebsocket::SendString(const string msg)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   if(StringLen(msg)<=0)
     {
      Print(__FUNCTION__, " Message buffer is empty ");
      return(false);
     }

   BYTE msg_array[];

   StringToCharArray(msg,msg_array,0,WHOLE_ARRAY);

   ArrayRemove(msg_array,ArraySize(msg_array)-1,1);

   DWORD len=(ArraySize(msg_array));

   return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

//+------------------------------------------------------------------+
//|Public method for sending data prepackaged in an array            |
//+------------------------------------------------------------------+
bool CWebsocket::Send(BYTE &buffer[])
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

调用 Poll() 方法会在底层发起异步读取操作,使WebSocket客户端进入POLLING状态。该状态表示客户端正在等待服务器的响应。

//+------------------------------------------------------------------+
//|  asynchronous read operation (polls for server response)         |
//+------------------------------------------------------------------+
ulong CWebsocket::Poll(void)
  {
   if(hWebSocket!=NULL)
      return client_poll(hWebSocket);
   else
      return WEBSOCKET_ERROR_INVALID_HANDLE;
  }

用户可通过以下两种方式确认客户端是否已接收并成功读取数据:

  • CallBackResult():此方法检查从回调函数接收到的最新通知。成功的读取操作应触发“读取完成”通知。
  • ReadAvailable():此方法返回内部缓冲区中当前可供读取的数据大小(以字节为单位)。

//+------------------------------------------------------------------+
//| call back notification                                           |
//+------------------------------------------------------------------+
ulong CWebsocket::CallBackResult(void)
  {
   if(hWebSocket!=NULL)
      return client_lastcallback_notification(hWebSocket);
   else
      return WINHTTP_CALLBACK_STATUS_DEFAULT;
  }
//+------------------------------------------------------------------+
//|  check if any data has read from the server                      |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadAvailable(void)
  {
   if(hWebSocket!=NULL)
      return(client_readable(hWebSocket));
   else
      return 0;
  }

随后,用户可通过Read()或ReadString()方法访问服务器传输的原始数据。两种方法均会返回接收到的数据大小。ReadString()需要传入一个字符串类型的引用变量,接收到的数据将被写入该变量;而Read()则将数据写入一个无符号字符数组中。

//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::Read(BYTE &buffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE &buffertype)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   ulong bytes_read_from_socket=0;

   clientread(buffer,buffertype,bytes_read_from_socket);

   return(bytes_read_from_socket);

  }
//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadString(string &_response)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   ulong bytes_read_from_socket=0;

   ZeroMemory(rxbuffer);

   WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype;

   clientread(rxbuffer,rbuffertype,bytes_read_from_socket);

   _response=(bytes_read_from_socket)?CharArrayToString(rxbuffer):"";

   return(bytes_read_from_socket);
  }

当不再需要WebSocket客户端时,可使用Close()方法终止与服务器的连接。Abort()方法与Close()的区别在于:前者通过强制关闭底层句柄来终止WebSocket连接,同时会将内部类属性的值重置为默认状态。该方法可显式调用以执行资源清理操作。

最后,WebSocketHandle()方法返回底层的HINTERNET类型WebSocket句柄。

//+------------------------------------------------------------------+
//| Closes a websocket client connection                             |
//+------------------------------------------------------------------+
void CWebsocket::Close(void)
  {
   if(!initialized || hWebSocket == NULL)
      return;
   else
      client_disconnect(hWebSocket);
  }
//+--------------------------------------------------------------------------+
//|method for abandoning a client connection. All previous server connection |
//|   parameters are reset to their default state                            |
//+--------------------------------------------------------------------------+
void CWebsocket::Abort(void)
  {
   client_reset(hWebSocket);
   reset();
  }
//+------------------------------------------------------------------+
//|   websocket handle                                               |
//+------------------------------------------------------------------+
HINTERNET CWebsocket::WebSocketHandle(void)
  {
   if(hWebSocket!=NULL)
      return client_websocket_handle(hWebSocket);
   else
      return NULL;
  }


使用DLL

本节将通过一个示例程序,演示如何使用asyncwinhttpwebsockets.dll动态链接库。该程序包含一个图形用户界面(GUI),用于连接至托管在https://echo.websocket.org的WebSocket回显服务(该服务专为测试WebSocket客户端实现而提供)。构建此应用程序需使用免费的简单快速GUI MQL5库。程序在MetaTrader 5环境中作为智能交易系统(EA)运行。用户界面包含两个按钮,分别用于建立和终止与指定服务器的连接。此外,还提供一个文本输入框,供用户输入要发送至服务器的消息。

WebSocket客户端执行的操作将被记录并显示,每条日志均带有时间戳,以标明操作发生的时间。以下是该程序的完整源代码:

//+------------------------------------------------------------------+
//|                                                         Echo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <EasyAndFastGUI\WndCreate.mqh>
#include <asyncwebsocket.mqh>
//+------------------------------------------------------------------+
//| Gui application class                                            |
//+------------------------------------------------------------------+
class CApp:public CWndCreate
  {
protected:

   CWindow           m_window;                   //main window

   CTextEdit         m_rx;                       //text input to specify messages to be sent

   CTable            m_tx;                       //text box displaying received messages from the server

   CButton           m_connect;                  //connect button

   CButton           m_disconnect;               //disconnect button

   CTimeCounter      m_timer_counter;                //On timer objects

   CWebsocket*        m_websocket;               //websocket connection
public:
                     CApp(void);                 //constructor
                    ~CApp(void);                 //destructor

   void              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);

   virtual void      OnEvent(const int id, const long &lparam, const double &dparam,const string &sparam);
   void              OnTimerEvent(void);
   bool              CreateGUI(void);

protected:

private:
   uint              m_row_index;
   void              EditTable(const string newtext);
  };
//+------------------------------------------------------------------+
//|  constructor                                                     |
//+------------------------------------------------------------------+
CApp::CApp(void)
  {
   m_row_index = 0;
   m_timer_counter.SetParameters(10,50);
   m_websocket = new CWebsocket();
  }
//+------------------------------------------------------------------+
//|   destructor                                                     |
//+------------------------------------------------------------------+
CApp::~CApp(void)
  {
   if(CheckPointer(m_websocket) == POINTER_DYNAMIC)
      delete m_websocket;
  }
//+------------------------------------------------------------------+
//| On initialization                                                |
//+------------------------------------------------------------------+
void CApp::OnInitEvent(void)
  {
  }
//+------------------------------------------------------------------+
//| On DeInitilization                                               |
//+------------------------------------------------------------------+
void CApp::OnDeinitEvent(const int reason)
  {
   CWndEvents::Destroy();
  }
//+------------------------------------------------------------------+
//| on timer event                                                   |
//+------------------------------------------------------------------+
void CApp::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();

   if(m_timer_counter.CheckTimeCounter())
     {

      ENUM_WEBSOCKET_STATE client_state = m_websocket.ClientState();
      ulong operation = m_websocket.CallBackResult();

      switch((int)operation)
        {
         case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
            if(client_state == CLOSED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnected]");
               m_websocket.Abort();
              }
            break;
         case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
            if(client_state!=POLLING)
              {
               m_websocket.Poll();
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Send Complete]");
              }
            break;

         case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
            if(m_websocket.ReadAvailable())
              {
               string response;
               m_websocket.ReadString(response);
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Received]-> "+response);
              }
            break;
         case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
            EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Error]-> "+m_websocket.LastErrorMessage());
            m_websocket.Abort();
            break;
         default:
            break;
        }
     }
  }
//+------------------------------------------------------------------+
//| create the gui                                                   |
//+------------------------------------------------------------------+
bool CApp::CreateGUI(void)
  {
//---check the websocket object
   if(CheckPointer(m_websocket) == POINTER_INVALID)
     {
      Print(__FUNCTION__," Failed to create websocket client object ", GetLastError());
      return false;
     }
//---initialize window creation
   if(!CWndCreate::CreateWindow(m_window,"Connect to https://echo.websocket.org Echo Server ",1,1,750,300,true,false,true,false))
      return(false);
//---
   if(!CWndCreate::CreateTextEdit(m_rx,"",m_window,0,false,0,25,750,750,"Click connect button below, input your message here, then press enter key to send"))
      return(false);
//---
   if(!CWndCreate::CreateButton(m_connect,"Connect",m_window,0,5,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
      return(false);
//---create text edit for width in frequency units
   if(!CWndCreate::CreateButton(m_disconnect,"Disonnect",m_window,0,500,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
      return(false);
//---create text edit for amount of padding
   string tableheader[1] = {"Client Operations Log"};
   if(!CWndCreate::CreateTable(m_tx,m_window,0,1,10,tableheader,5,75,0,0,true,true,5))
      return(false);
//---
   m_tx.TextAlign(0,ALIGN_LEFT);
   m_tx.ShowTooltip(false);
   m_tx.DataType(0,TYPE_STRING);
   m_tx.IsDropdown(false);
   m_tx.SelectableRow(false);
   int cwidth[1] = {740};
   m_tx.ColumnsWidth(cwidth);
//---init events
   CWndEvents::CompletedGUI();
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| edit the table                                                   |
//+------------------------------------------------------------------+
void CApp::EditTable(const string newtext)
  {
   if(newtext==NULL)
      return;

   if((m_row_index+1)==m_tx.RowsTotal())
     {
      m_tx.AddRow(m_row_index+1,true);
      m_tx.Update();
     }

   m_tx.SetValue(0,m_row_index++,newtext,0,true);
   m_tx.Update(true);
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CApp::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      if(lparam==m_rx.Id())
        {
         if(m_websocket.ClientState() == CONNECTED)
           {
            string textinput = m_rx.GetValue();
            if(StringLen(textinput)>0)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Sending]-> "+textinput);
               m_websocket.SendString(textinput);
              }
           }
        }
      return;
     }
   else
      if(id == CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
        {
         if(lparam==m_connect.Id())
           {
            if(m_websocket.ClientState() != CONNECTED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connecting]");
               if(m_websocket.Connect("https://echo.websocket.org/"))
                 {
                  EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connected]");
                  m_websocket.Poll();
                 }
               else
                  EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[FailedToConnect]");
              }
            return;
           }
         if(lparam==m_disconnect.Id())
           {
            if(m_websocket.ClientState() != CLOSED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnecting]");
               m_websocket.Close();
              }
           }
         return;
        }
  }
//+------------------------------------------------------------------+
CApp app;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   ulong tick_counter=::GetTickCount();
//---
   app.OnInitEvent();
//---
   if(!app.CreateGUI())
     {
      ::Print(__FUNCTION__," > error");
      return(INIT_FAILED);
     }

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   app.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   app.OnTimerEvent();
  }
//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   app.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

下图展示了该程序的运行方式。

回显 EA演示


结论

本文详细阐述了如何基于WinHTTP库以异步模式为MetaTrader 5开发WebSocket客户端。为此构建了一个专用类封装相关功能,并通过一个与echo.websocket.org回显服务器交互的EA演示了其实现。完整源代码(含动态链接库)已随附于补充材料中。具体而言,C++源文件及构建配置文件CMakeLists.txt位于指定的C++目录下。补充材料中的MQL5目录则包含预编译的asyncwinhttpwebsockets.dll文件,可直接部署使用。

如果用户希望从提供的源码自行编译客户端库,必需使用CMake构建系统。安装CMake后,可启动图形界面工具(cmake-gui),用户需要指定源码目录,对应CMakeLists.txt文件所在路径(即 MqlWebsocketsClientDLL\Source\C++),并且指定独立的构建目录,可创建于任意位置。

随后点击"Configure"按钮启动配置流程。弹出对话框提示"Specify the generator for this project"时,需选择系统中已安装的对应版本Visual Studio。在"Optional platform for generator"设置中,用户可选择"Win32"编译32位DLL;如果留空,则默认生成64位版本。点击 "Finish" 后,CMake将处理初始配置。

此时会弹出错误提示,指出需在CMakeLists.txt文件中配置特定内容。为解决该问题,用户应找到标记为"ADDITIONAL_LIBRARY_DEPENDENCIES"的内容,点击相邻字段并导航至包含winhttp.lib文件的目录。

接着找到"OUTPUT_DIRECTORY_Xxx_RELEASE"内容("Xxx"表示架构,如X64或X86),将对应路径设置为MetaTrader安装目录下的"Libraries"文件夹。

完成上述配置后,当再次点击"Configure"时,应当无错误提示。随后点击"Generate"生成构建文件,成功后会激活"Open Project"按钮,点击即可打开生成的Visual Studio项目文件。

在Visual Studio中,选择"Build" → "Build Solution"编译DLL。数秒后即可获得生成的动态链接库文件。

文件或文件夹名称
描述
MqlWebsocketsClientDLL\Source\C++
包含asycnwinhttpwebsockets.dll的完整源代码文件的文件夹
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh 包含导入指令,列出asyncwinhttpwebsockets.dll公开的所有函数
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh 包含封装底层DLL函数功能的CWebsocket类定义。
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 演示该DLL应用的EA代码文件
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 演示该DLL应用的已编译EA
MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll 提供异步WinHTTP WebSocket功能的已编译DLL

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17877

附加的文件 |
asyncwinhttp.mqh (13.42 KB)
asyncwebsocket.mqh (19.09 KB)
Echo.mq5 (10.84 KB)
Echo.ex5 (306.48 KB)
最近评论 | 前往讨论 (9)
Shephard Mukachi
Shephard Mukachi | 2 5月 2025 在 21:17
Ryan L Johnson #:

在以下网站查看一些示例代码可能会有所帮助:

同样的基本前提也适用于 EA。

这是一个非常好的想法。

CapeCoddah
CapeCoddah | 15 5月 2025 在 23:47

感谢两位对我问题的回复。 我一定是错过了重载函数定义,只读到了第一个。 你们是否知道终端是否足够智能,可以并行处理 iCustom 调用,以最大限度地利用处理器,因为我计划改变 28 个交易对中每个交易的符号参数,并计划像 Brooky Trend Strength 一样使用多个 iCustom 调用。

另外,你们谁能告诉我在哪里可以发表对 MQ5 中错误的意见,以及对 Mq 管理员的建议。 我已经发现了一些问题,最近的问题是终端和策略测试器 之间的Bars 差异。 另外,我有一个 3 屏幕设置,主显示屏在最左边。 尝试移动一个面板,如导航器或市场面板。拖动鼠标指针是在最左边的屏幕上,但拖动的面板却在中间。 当鼠标移动一个像素,然后切换显示屏将面板移动一个像素,然后再移动回来时,我认为终端或 Windows 都疯了。

Silk Road Trading LLC
Ryan L Johnson | 16 5月 2025 在 00:13
CapeCoddah 策略测试器 之间的Bars 差异。

如果缺少价格数据,Bars() 就会打嗝,而 rates_total 则不会。如果我没记错过去读过的文章,Bars() 可以通过参考时间戳来修复。也许值得一试。

CapeCoddah#:
我有一个 3 屏幕设置,主显示屏在最左边。 尝试将一个面板(如导航器或市场面板)从左边移到右边非常麻烦。 拖动鼠标指针在最左边的屏幕上,但拖动的面板在中间。 当鼠标移动一个像素,然后切换显示屏将面板移动一个像素再移回来时,我认为终端或 Windows 都疯了。

我真的不知道该怎么办。我有 3 台电脑,每台电脑都有自己的显示器和终端。我知道 Windows 一般都有多显示器显示设置,包括画中画可能是一种解决方法。

能否请其他真正在一台机器上使用多台显示器的人在此提供帮助?

CapeCoddah
CapeCoddah | 16 5月 2025 在 09:42

很棒的信息

我的问题是,在终端中两者是相同的,但在STrategy Tester Visualize 中,Bars 是更大的一个,这导致我没有将文档读完。 我将采纳您的意见并将其用于 iCustom。 我推测,必须为每种符号和时间规格组合设置一个单独的 iCustom 地址。

另外,有没有办法让 EA 在策略测试器的屏幕上显示文本? 在 Mq4 中,它可以自动显示,但现在不行了。 我使用大量类对象来显示信息,在模板中放置第二个副本会使策略测试器的运行速度更慢。

关于 3 面板显示,我认为问题在于当鼠标从屏幕 2 移动到屏幕 1 时,终端无法正确更新显示器位置。

我有两台迷你电脑,每台都支持 3 个显示器,因此我将 3 个屏幕连接到两台迷你电脑上,一台电脑使用 HDMI1,另一台电脑使用 HDMI2。 虽然必须确保遥控器正确配置为只控制一个显示器(请致电亚马逊支持中心),但与 43 英寸 Fire 电视机配合使用效果非常好。 唯一的缺点是开关按钮会关闭所有显示器,有时我需要拔掉插头来同步电源。


科达角

Silk Road Trading LLC
Ryan L Johnson | 16 5月 2025 在 19:58
CapeCoddah STrategy Tester Visualize 中,Bars 却大了一圈,这导致我没有将文档读完。 我将采纳您的意见并将其用于 iCustom。 我推测,符号和时间规格的每种组合都必须有一个单独的 iCustom 地址。

  1. iCustom() 的多个实例可以重复使用单个目录中的单个指标文件。
  2. 多个 CopyBuffer() 实例可以重复使用单个指标句柄。
  3. 我现在明白你为什么要使用 Bars(),因为 rates_total 本身仅限于单个时间框架。您大概是在每个时间框架的单独循环中使用 Bars()。

CapeCoddah#:
另外,有没有办法让 EA 在策略测试器的屏幕上显示文本? 在 Mq4 中,它可以自动显示,但现在不行了。 我使用大量类对象来显示信息,在模板中放置第二个副本会使策略测试器更慢。
据我所知不会。您已经在使用我从测试可视化 MT5 帮助页面中了解到的唯一方法。
CapeCoddah#:
关于 3 面板显示,我认为问题在于当鼠标从屏幕 2 移动到屏幕 1 时,终端没有正确更新显示器位置。
遗憾的是,我无法用自己的设置来测试这个问题。您是否在所有显示器上拉伸了一个 MT5 终端屏幕?我见过其他人用这种方法解决问题。
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
开发多币种 EA 交易(第 24 部分):添加新策略(一) 开发多币种 EA 交易(第 24 部分):添加新策略(一)
在本文中,我们将研究如何将新策略连接到我们创建的自动优化系统。让我们看看我们需要创建哪些类型的 EA,以及是否可以在不更改 EA 库文件的情况下完成,或者尽量减少必要的更改。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA) 在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA)
本文详细介绍如何利用第一篇开发的状态检测器,构建一个自适应的智能交易系统(MarketRegimeEA)。该系统能够根据趋势、震荡或高波动市场,自动切换交易策略与风险参数。文中涵盖了实用的参数优化、状态过渡处理以及多时间周期指标的应用。