
Websockets for MetaTrader 5: Asynchronous client connections with the Windows API
Introduction
The article, "WebSockets for MetaTrader 5: Using the Windows API", illustrated the utilization of the Windows API for the implementation of a websocket client within MetaTrader 5 applications. The implementation presented there was constrained by its synchronous operational mode.
In this article, we revisit the application of the Windows API to construct a websocket client for MetaTrader 5 programs, with the objective of achieving asynchronous client functionality. A practical methodology for realizing this objective involves the creation of a custom dynamically linked library (DLL) that exports functions suitable for integration with MetaTrader 5 applications.
Accordingly, this article will discuss the development process of the DLL and subsequently present a demonstration of its application through an MetaTrader 5 program example.
WinHTTP asynchronous mode
The prerequisites for asynchronous operation within the WinHTTP library, as delineated in its documentation, are twofold. Firstly, during the invocation of the WinHTTPOpen function, the session handle must be configured with either the WINHTTP_FLAG_ASYNC or WINHTTP_FLAG_SECURE_DEFAULTS flag.
// 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;
After the establishment of a valid session handle, users are required to register a callback function to receive notifications pertaining to various events associated with specific WinHTTP function calls. This status callback function is informed of the progress of asynchronous operations via notification flags.
Registration of the callback function is accomplished through the WinHttpSetStatusCallback function, which also permits the specification of notification flags that the callback will manage. Users can elect to subscribe to a comprehensive set of notifications or a more limited subset. Furthermore, distinct callback functions can be designated for session, request, and websocket handles, respectively.
It is important to note that the registration of a callback function immediately following session handle creation is not mandatory; the WinHttpSetStatusCallback function can be invoked for any valid HINTERNET handle at any stage before or during websocket connection initialization.
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; }
The signature of the user-defined callback function incorporates a parameter that points to a user-defined data structure, commonly referred to as a context value. This mechanism facilitates the transfer of data from the callback function.
The context value must be specified before callback function registration through a call to the WinHttpSetOption function with the WINHTTP_OPTION_CONTEXT_VALUE option flag. It is pertinent to acknowledge that empirical testing encountered difficulties in reliably retrieving a registered context value via this method.
While the possibility of implementation error cannot be entirely discounted, consistent failure necessitated the adoption of a global variable as an alternative, a detail that will be dealt with in the subsequent discussion of DLL implementation.
Finally, a crucial consideration regarding the user-defined callback function is the requirement for thread safety. However, given that the present work involves the creation of a DLL for use within the MetaTrader 5 environment, this constraint can be relaxed. This is due to the inherent single-threaded nature of MetaTrader 5 programs in general, and while DLL code executes within the thread pool of the loading process, on MetaTrader 5 programs, only a single thread is active.
void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
Implementing the DLL
The DLL is constructed within the Visual Studio environment using the C++ programming language. This process necessitates the installation of the "C++ Desktop development" workload in Visual Studio, alongside either the Windows 10 or Windows 11 Software Development Kit (SDK). The Windows SDK is a prerequisite, as it furnishes the WinHTTP library file (.lib), to which the DLL will be linked during compilation. The resultant DLL comprises at least three fundamental components.
The first is a class that encapsulates the essential WinHTTP websocket client functionality. The second is a singular callback function that operates alongside a global variable, facilitating the manipulation of a websocket connection both within the callback function's scope and externally. The third component consists of a set of simplified function wrappers that will be exposed by the DLL for utilization in MetaTrader 5 programs. The implementation commences with the code defined in the asyncwebsocketclient.h header file.
This header file begins by declaring the WebSocketClient class, where each instance represents an individual client connection.
// 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); };
Alongside the class, the structure, Frame is defined to represent a message frame.
struct Frame { std::vector<BYTE>frame_buffer; WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type; DWORD frame_size; };
Furthermore, the enumeration ENUM_WEBSOCKET_STATE is declared to describe the various states of a websocket connection.
// client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
Next, asyncwebsocketclient.h declares a global variable named clients. This variable is a container, specifically a map, designed to store active websocket connections. The global scope of this map container ensures its accessibility to any callback function defined within the library.
// container for websocket objects accessible to callback function extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;
The asyncwebsocketclient.h file concludes by defining a set of functions qualified by WEBSOCK_API. This qualifier serves to mark these functions for export by the DLL. These functions constitute the aforementioned function wrappers and represent the interface through which developers will interact with the DLL within their MetaTrader 5 applications.
// 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);
Examination of the WebSocketCallback() function definition reveals the utilization of the global clients variable for managing notifications. The current implementation is configured to handle what is referred to as completion notifications, in the WinHTTP documentation. These notifications are triggered upon the successful completion of any asynchronous operation. For instance, WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE signals the completion of a send operation, while WINHTTP_CALLBACK_STATUS_READ_COMPLETE indicates the completion of a read operation.
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; } } }
The hInternet argument of the callback function serves as the HINTERNET handle upon which the callback was initially registered. This handle is employed to index a member within the global map container, clients, yielding a pointer to a WebSocketClient instance. The specific callback notification is conveyed through the dwInternetStatus argument. Notably, the data represented by the lpvStatusInformation and dwStatusInformationLength arguments varies according to the value of dwInternetStatus.
For instance, when dwInternetStatus assumes the values WINHTTP_CALLBACK_STATUS_READ_COMPLETE or WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE, the lpvStatusInformation parameter contains a pointer to a WINHTTP_WEB_SOCKET_STATUS structure, with dwStatusInformationLength indicating the size of the referenced data.
Our implementation selectively processes a subset of notifications provided by this callback function, each resulting in a modification of the state of the corresponding WebSocketClient instance. Specifically, the OnCallBack() method captures the status codes associated with these notifications. This information kept in the WebSocketClient instance where it can be exposed to users through a wrapper function.
VOID WebSocketClient::OnCallBack(const DWORD operation)
{
completed_websocket_operation = operation;
}
The OnReadComplete() method within the WebSocketClient class is responsible for transferring raw data into a queued buffer of frames, from which users can subsequently query the availability of data for retrieval.
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); }
The OnSendComplete() method updates internal fields that flag a successful send operation, which also triggers a change in the state of the websocket client.
VOID WebSocketClient::OnSendComplete(const DWORD sent) { bytesTX = sent; status = ENUM_WEBSOCKET_STATE::CONNECTED; return; }
Finally, the OnError() method captures any error-related information provided via the lpvStatusInformation argument.
VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result) { SetError(result->dwError); Reset(false); }
Establishing a connection to a server is accomplished via the client_connect() function. This function is invoked with the server address (as a string), the port number (as an integer), a boolean value specifying whether the connection should be secure, and a pointer to an HINTERNET value. The function returns a DWORD value as well setting the HINTERNET argument. In the event of any error during the connection process, the function will set the HINTERNET argument to NULL and return a non-zero error code.
Internally, the client_connect() function initializes an instance of the WebSocketClient class and utilizes it to establish the connection. Upon successful connection establishment, a pointer to this WebSocketClient instance is stored in the global clients container, with the instance's websocket handle serving as the unique key. This websocket handle is employed to uniquely identify a specific websocket connection, both within the DLL's internal operations and externally by the calling MetaTrader 5 program.
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; }
The initialization and establishment of websocket connections are managed by the Connect() method of the WebSocketClient class. The connection process is initially performed synchronously. Thereafter, the callback function is registered on the websocket handle through a call to the EnableCallBack() method, enabling asynchronous event notifications.
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; }
Upon establishing a connection to a server and obtaining a valid handle, users can initiate communication with the remote endpoint. Transmitting data to the server is performed through a call to the client_send() function. This function requires the following parameters: a valid websocket handle, a WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration value specifying the type of websocket frame to be transmitted, a BYTE array containing the data payload, and an ulong argument indicating the size of the data array. The function returns a zero value if no immediate errors are encountered; otherwise, it returns a specific error code.
Internally, the WinHttpWebSocketSend() function is invoked asynchronously. Consequently, the return value of client_send() represents an intermediate status, signifying the absence of preliminary errors during the setup of the send operation. The outcome of the actual data transmission is not returned synchronously. Instead, the result is communicated asynchronously via a notification accessible through the registered callback function. In the context of a successful send operation, a WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE notification is anticipated. Conversely, if an error occurs during any operation (send or receive), a WINHTTP_CALLBACK_STATUS_REQUEST_ERROR notification is typically propagated to the callback function.
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; }
Notifications received by the internal callback function can be retrieved using the client_lastcallback_notification() function. This function returns the most recent notification received by the callback for a specific connection, identified by the websocket handle provided as its sole argument. The subsequent code snippet illustrates a potential approach to handling these notifications within an MetaTrader 5 program. The symbolic constants corresponding to these notifications are defined in the asyncwinhttp.mqh file, which are derived from the original winhttp.h header file.
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; }
Receiving data transmitted by the server initially requires placing the client in what is referred to as a polling state by invoking the client_poll() function. This action internally calls the WinHttpWebSocketReceive() function within the WebSocketClient class. Similar to the send operation, the WinHTTP function is invoked asynchronously, resulting in the immediate return of an intermediate status.
The WebSocketClient class incorporates internal buffers to accommodate the raw data upon its arrival. Once a read operation is successfully completed, this data is enqueued within an internal data structure. This process is managed by the OnReadComplete() method of the WebSocketClient class. Upon completion of a read operation, the state of the websocket connection transitions, and it ceases to actively "listen" for incoming messages.
This implies that an asynchronous read request is not continuous and does not represent persistent polling. To retrieve subsequent messages from the server, the client_poll() function must be invoked again. Essentially, calling client_poll() places the websocket client in a temporary, non-blocking polling state, capturing data when it becomes available and subsequently triggering the WINHTTP_CALLBACK_STATUS_READ_COMPLETE notification.
The current state of the websocket client can be queried by calling the client_status() function, which returns a value of the ENUM_WEBSOCKET_STATE enumeration type.
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; }
The retrieval of raw data received from the server is facilitated by the client_read() function, which accepts the following arguments:
- a valid HINTERNET websocket handle,
- a reference to a pre-allocated BYTE array, an ulong value specifying the size of the aforementioned array,
- a reference to a WINHTTP_WEB_SOCKET_BUFFER_TYPE value.
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; }
Error codes can be obtained by calling the client_lasterror() function. The function returns a DWORD value of the last error encountered. Users can also obtain the current websocket handle value with client_websocket_handle(). This could be useful when trying to ascertain if a handle has been closed or not.
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; }
Graceful termination of a connection to a server is initiated by invoking the client_disconnect() function. This function does not return a value. However, it immediately transitions the state of the websocket client to a closing state. If a corresponding close frame is subsequently received from the server, the WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE notification will be triggered, thereby altering the websocket state to closed.
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; }
The final function exported by the DLL is client_reset(). Ideally, this function should be called after disconnecting from a server. Its purpose is to deallocate the internal memory buffers associated with the closed connection. While not strictly mandatory, invoking this function can be beneficial for reclaiming memory resources that may be required elsewhere during program execution. Calling client_reset() effectively invalidates all data associated with the specified websocket handle, including error codes, error messages, and any unread data remaining in the internal queue of frames.
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
{
if(clients.find(websocket_handle) != clients.end())
{
clients[websocket_handle]->Free();
clients.erase(websocket_handle);
}
}
Redefining the CWebsocket class
Before examining an MetaTrader 5 application that utilizes the functions detailed in the preceding section, a redefinition of the CWebsocket class, previously discussed in the aforementioned article, will be undertaken. This redefinition will repurpose the class to leverage the newly developed asynchronous websocket client. The source code for this adaptation is located in the asyncwebsocket.mqh file.
The ENUM_WEBSOCKET_STATE enumeration has been expanded to incorporate additional states that reflect the asynchronous nature of the client. The POLLING state is engaged when an asynchronous read operation is initiated. Upon the underlying socket receiving data and making it available for retrieval, the callback function signals the completion of the asynchronous read operation, and the state of the websocket client transitions to its default state: CONNECTED. Similarly, an asynchronous send operation transitions the state to SENDING. The outcome of this operation is communicated asynchronously via the callback function, whereby a successful transmission results in a return to the default CONNECTED state.
//+------------------------------------------------------------------+ //| client state enumeration | //+------------------------------------------------------------------+ // client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
Several new methods have been integrated into the CWebsocket class to accommodate its enhanced capabilities. The remaining methods retain their original signatures, with only their internal implementations modified to incorporate the new DLL dependency. These methods are as follows:
Connect(): This method serves as the initial point of interaction for establishing a connection to a server. It accepts the following parameters:- _serveraddress: The complete address of the server (string data type).
- _port: The server's port number (ushort data type).
- _secure: A boolean value indicating whether a secure connection should be established (boolean data type).
The implementation of this method has been significantly simplified, as the majority of the connection establishment logic is now handled by the underlying 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); }
If the Connect() method returns a boolean true value, indicating a successful connection, data transmission can commence via the WebSocket client. Two methods are provided for this purpose:
- SendString(): This method accepts a string as input.
- Send(): This method accepts an unsigned character array as its sole parameter.
Both methods return a boolean true upon successful initiation of the send operation and internally invoke the private method clientsend(), which manages all send operations for the class.
//+------------------------------------------------------------------+ //|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)); }
Invoking the Poll() method initiates an asynchronous read operation at the lower level, transitioning the websocket client into the POLLING state. This state signifies that the client is awaiting a response from the server.
//+------------------------------------------------------------------+ //| asynchronous read operation (polls for server response) | //+------------------------------------------------------------------+ ulong CWebsocket::Poll(void) { if(hWebSocket!=NULL) return client_poll(hWebSocket); else return WEBSOCKET_ERROR_INVALID_HANDLE; }
To ascertain whether data has been received and successfully read by the client, two options are available to the user:
- CallBackResult(): This method checks the last notification received from the callback function. A successful read operation should result in a read complete notification.
- ReadAvailable(): This method returns the size (in bytes) of the data currently available for retrieval in the internal buffer.
//+------------------------------------------------------------------+ //| 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; }
The raw data transmitted by the server can then be accessed using either the Read() or ReadString() methods. Both methods return the size of the data received. ReadString() requires a string variable passed by reference, into which the received data will be written, whereas Read() writes the data to an unsigned character array.
//+------------------------------------------------------------------+ //|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); }
When the WebSocket client is no longer required, the connection to the server can be terminated using either the Close() method. The Abort() method differs from Close() in that it forces closure of a websocket connection by closing the underlying handles, which also resets the values of internal class properties to their default states. The method can be explicitly called to perform resource cleanup.
Finally, the WebSocketHandle() method returns the underlying HINTERNET websocket handle.
//+------------------------------------------------------------------+ //| 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; }
Using the DLL
This section presents an illustrative program designed to demonstrate the utilization of the asyncwinhttpwebsockets.dll. The program incorporates a graphical user interface (GUI) that establishes a connection to the websocket echo service hosted at https://echo.websocket.org, a resource specifically provisioned for testing websocket client implementations. Construction of this application necessitates the freely available Easy And Fast GUI MQL5 library. The program is implemented as an Expert Advisor (EA) within the MetaTrader 5 environment. The user interface features two buttons, enabling the establishment and termination of a connection to the designated server. Additionally, a text input field is provided to allow users to enter messages for transmission to the server.
The operations performed by the websocket client are recorded and displayed, with each log entry timestamped to indicate the time of occurrence. The source code for this program is provided below.
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
The graphic below shows how the program works.
Conclusion
This article detailed the development of a WebSocket client for MetaTrader 5 utilizing the WinHTTP library in an asynchronous operational mode. A dedicated class was constructed to encapsulate this functionality, and its implementation was demonstrated within an Expert Advisor (EA) designed to interact with the echo server hosted at echo.websocket.org. The complete source code, including that of the dynamic link library, is provided in the supplementary materials. Specifically, the C++ source files, along with a CMakeLists.txt build configuration file, are located within the designated C++ directory. Additionally, the MQL5 directory in the supplementary materials contains a pre-compiled asyncwinhttpwebsockets.dll file for immediate deployment.
For users wishing to build the client library from the provided source code, the CMake build system is required. If CMake is installed, the graphical user interface (cmake-gui) can be invoked. The user must then specify the source code directory, which corresponds to the location of the CMakeLists.txt file (i.e., MqlWebsocketsClientDLL\Source\C++), and a separate build directory, which can be created at any desired location.
Thereafter, clicking the "Configure" button will initiate the configuration process. A dialogue window will prompt the user to "Specify the generator for this project," where the appropriate version of Visual Studio installed on the system should be selected. Under the "Optional platform for generator" setting, users can specify "Win32" to compile a 32-bit version of the DLL; otherwise, leaving this field blank will result in a default 64-bit compilation. Upon clicking "Finish," CMake will process the initial configuration.
An error notification will then appear, indicating the necessity to configure specific entries within the CMakeLists.txt file. To address this, the user should locate the entry labeled "ADDITIONAL_LIBRARY_DEPENDENCIES," click in the adjacent field, and navigate to the directory containing the winhttp.lib file.
Following this, the user should locate the entry labeled "OUTPUT_DIRECTORY_Xxx_RELEASE" (where "Xxx" denotes the architecture, X64 or X86) and set the corresponding path to the "Libraries" folder of a MetaTrader installation.
After configuring these options, clicking "Configure" again should complete the configuration process without further error notifications. The build file can then be generated by clicking "Generate." Successful generation will activate the "Open Project" button, which, when clicked, will open the generated Visual Studio project file.
To build the DLL, the user should select "Build" then "Build Solution" within Visual Studio. The resulting DLL will be available within a few seconds.
FIle or Folder name | Description |
---|---|
MqlWebsocketsClientDLL\Source\C++ | Folder contains the full source code files for asycnwinhttpwebsockets.dll |
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh | Contains the import directive, that lists all functions exposed by the asycnwinhttpwebsockets.dll |
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh | Contains the definition of the CWebsocket class that wraps the functionality provided by the underlying DLL functions. |
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 | The code file for an EA that demonstraites the application of the DLL |
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 | The compiled EA that demonstraites the application of the DLL |
MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll | The compiled DLL providing asynchronous Winhttp websocket functionality |





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use