Websockets para MetaTrader 5: conexiones de cliente asíncronas con la API de Windows
Introducción
El artículo «WebSockets para MetaTrader 5: uso de la API de Windows» ilustraba la utilización de la API de Windows para la implementación de un cliente de WebSocket en aplicaciones MetaTrader 5. La implementación allí presentada estaba limitada por su modo operativo sincrónico.
En este artículo, revisamos la aplicación de la API de Windows para construir un cliente websocket para programas MetaTrader 5, con el objetivo de lograr una funcionalidad de cliente asincrónica. Una metodología práctica para lograr este objetivo implica la creación de una biblioteca vinculada dinámicamente (DLL) personalizada que exporta funciones adecuadas para la integración con aplicaciones MetaTrader 5.
En consecuencia, este artículo discutirá el proceso de desarrollo de la DLL y posteriormente presentará una demostración de su aplicación a través de un ejemplo de programa MetaTrader 5.
Modo asincrónico WinHTTP
Los requisitos previos para el funcionamiento asíncrono dentro de la biblioteca WinHTTP, tal y como se describe en su documentación, son dos. En primer lugar, durante la invocación de la función WinHTTPOpen, el identificador de sesión debe configurarse con el indicador WINHTTP_FLAG_ASYNC o 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;
Después de establecer un identificador de sesión válido, los usuarios deben registrar una función de devolución de llamada para recibir notificaciones relacionadas con diversos eventos asociados con llamadas de funciones específicas de WinHTTP. Esta función de devolución de llamada de estado se informa del progreso de las operaciones asincrónicas a través de indicadores de notificación.
El registro de la función de devolución de llamada se realiza a través de la función WinHttpSetStatusCallback, que también permite especificar los indicadores de notificación que gestionará la devolución de llamada. Los usuarios pueden elegir suscribirse a un conjunto completo de notificaciones o a un subconjunto más limitado. Además, se pueden designar funciones de devolución de llamada distintas para los controladores de sesión, solicitud y websocket, respectivamente.
Es importante tener en cuenta que el registro de una función de devolución de llamada inmediatamente después de la creación del identificador de sesión no es obligatorio; la función WinHttpSetStatusCallback se puede invocar para cualquier identificador HINTERNET válido en cualquier etapa antes o durante la inicialización de la conexión websocket.
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; }
La firma de la función de devolución de llamada definida por el usuario incorpora un parámetro que apunta a una estructura de datos definida por el usuario, comúnmente denominada valor de contexto. Este mecanismo facilita la transferencia de datos desde la función de devolución de llamada.
El valor de contexto debe especificarse antes del registro de la función de devolución de llamada a través de una llamada a la función WinHttpSetOption con el indicador de opción WINHTTP_OPTION_CONTEXT_VALUE. Es pertinente reconocer que las pruebas empíricas encontraron dificultades para recuperar de manera confiable un valor de contexto registrado a través de este método.
Si bien no se puede descartar por completo la posibilidad de un error de implementación, la falla constante hizo necesaria la adopción de una variable global como alternativa, un detalle que se tratará en la discusión posterior sobre la implementación de DLL.
Por último, una consideración crucial con respecto a la función de devolución de llamada definida por el usuario es el requisito de seguridad del hilo. Sin embargo, dado que el presente trabajo implica la creación de una DLL para su uso dentro del entorno MetaTrader 5, esta restricción puede relajarse. Esto se debe a la naturaleza inherente de un solo hilo de los programas MetaTrader 5 en general, y mientras que el código DLL se ejecuta dentro del grupo de hilos del proceso de carga, en los programas MetaTrader 5 solo un hilo está activo.
void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) Implementando la DLL
La DLL se construye dentro del entorno de Visual Studio utilizando el lenguaje de programación C++. Este proceso requiere la instalación de la carga de trabajo "Desarrollo de escritorio C++" en Visual Studio, junto con el Kit de desarrollo de software (SDK) de Windows 10 o Windows 11. El SDK de Windows es un requisito previo, ya que proporciona el archivo de biblioteca WinHTTP (.lib), al que se vinculará la DLL durante la compilación. La DLL resultante comprende al menos tres componentes fundamentales.
La primera es una clase que encapsula la funcionalidad esencial del cliente websocket WinHTTP. La segunda es una función de devolución de llamada singular que opera junto con una variable global, lo que facilita la manipulación de una conexión websocket tanto dentro del alcance de la función de devolución de llamada como externamente. El tercer componente consiste en un conjunto de envoltorios de funciones simplificados que la DLL expondrá para su uso en programas MetaTrader 5. La implementación comienza con el código definido en el archivo de encabezado asyncwebsocketclient.h.
Este archivo de encabezado comienza declarando la clase WebSocketClient, donde cada instancia representa una conexión de cliente individual.
// 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); };
Junto a la clase, se define la estructura Frame para representar un marco de mensaje.
struct Frame { std::vector<BYTE>frame_buffer; WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type; DWORD frame_size; };
Además, se declara la enumeración ENUM_WEBSOCKET_STATE para describir los distintos estados de una conexión websocket.
// client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
A continuación, asyncwebsocketclient.h declara una variable global denominada clientes. Esta variable es un contenedor, específicamente un mapa, diseñado para almacenar conexiones websocket activas. El alcance global de este contenedor de mapas garantiza su accesibilidad a cualquier función de devolución de llamada definida dentro de la biblioteca.
// container for websocket objects accessible to callback function extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;
El archivo asyncwebsocketclient.h concluye definiendo un conjunto de funciones calificadas por WEBSOCK_API. Este calificador sirve para marcar estas funciones para su exportación por la DLL. Estas funciones constituyen los envoltorios de funciones mencionados anteriormente y representan la interfaz a través de la cual los desarrolladores interactuarán con la DLL dentro de sus aplicaciones MetaTrader 5.
// 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);
El examen de la definición de la función WebSocketCallback() revela la utilización de la variable global de clientes para gestionar notificaciones. La implementación actual está configurada para manejar lo que se conoce como notificaciones de finalización, en la documentación de WinHTTP. Estas notificaciones se activan cuando se completa con éxito cualquier operación asincrónica. Por ejemplo, WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE señala la finalización de una operación de envío, mientras que WINHTTP_CALLBACK_STATUS_READ_COMPLETE indica la finalización de una operación de lectura.
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; } } }
El argumento hInternet de la función de devolución de llamada sirve como identificador HINTERNET en el que se registró inicialmente la devolución de llamada. Este identificador se utiliza para indexar un miembro dentro del contenedor de mapas global, clientes, generando un puntero a una instancia de WebSocketClient. La notificación de devolución de llamada específica se transmite a través del argumento dwInternetStatus. En particular, los datos representados por los argumentos lpvStatusInformation y dwStatusInformationLength varían según el valor de dwInternetStatus.
Por ejemplo, cuando dwInternetStatus asume los valores WINHTTP_CALLBACK_STATUS_READ_COMPLETE o WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE, el parámetro lpvStatusInformation contiene un puntero a una estructura WINHTTP_WEB_SOCKET_STATUS, donde dwStatusInformationLength indica el tamaño de los datos referenciados.
Nuestra implementación procesa selectivamente un subconjunto de notificaciones proporcionadas por esta función de devolución de llamada, cada una de las cuales genera una modificación del estado de la instancia WebSocketClient correspondiente. Específicamente, el método OnCallBack() captura los códigos de estado asociados con estas notificaciones. Esta información se guarda en la instancia WebSocketClient, donde puede exponerse a los usuarios a través de una función contenedora.
VOID WebSocketClient::OnCallBack(const DWORD operation)
{
completed_websocket_operation = operation;
} El método OnReadComplete() dentro de la clase WebSocketClient es responsable de transferir datos sin procesar a un búfer de marcos en cola, desde el cual los usuarios pueden consultar posteriormente la disponibilidad de datos para su recuperación.
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); }
El método OnSendComplete() actualiza los campos internos que marcan una operación de envío exitosa, lo que también desencadena un cambio en el estado del cliente websocket.
VOID WebSocketClient::OnSendComplete(const DWORD sent) { bytesTX = sent; status = ENUM_WEBSOCKET_STATE::CONNECTED; return; }
Finalmente, el método OnError() captura cualquier información relacionada con el error proporcionada a través del argumento lpvStatusInformation.
VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result) { SetError(result->dwError); Reset(false); }
El establecimiento de una conexión con un servidor se logra a través de la función client_connect(). Esta función se invoca con la dirección del servidor (como una cadena), el número de puerto (como un entero), un valor booleano que especifica si la conexión debe ser segura y un puntero a un valor HINTERNET. La función devuelve un valor DWORD además de establecer el argumento HINTERNET. En caso de que se produzca algún error durante el proceso de conexión, la función establecerá el argumento HINTERNET en NULL y devolverá un código de error distinto de cero.
Internamente, la función client_connect() inicializa una instancia de la clase WebSocketClient y la utiliza para establecer la conexión. Una vez establecida la conexión, se almacena un puntero a esta instancia de WebSocketClient en el contenedor de clientes global, y el identificador de websocket de la instancia sirve como clave única. Este controlador de websocket se utiliza para identificar de forma única una conexión websocket específica, tanto dentro de las operaciones internas de la DLL como externamente por el programa MetaTrader 5 que la llama.
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; }
La inicialización y el establecimiento de conexiones websocket se gestionan mediante el método Connect() de la clase WebSocketClient. El proceso de conexión se realiza inicialmente de forma sincrónica. Posteriormente, la función de devolución de llamada se registra en el identificador del websocket a través de una llamada al método EnableCallBack(), lo que habilita notificaciones de eventos asincrónicos.
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; }
Al establecer una conexión con un servidor y obtener un identificador válido, los usuarios pueden iniciar la comunicación con el punto final remoto. La transmisión de datos al servidor se realiza mediante una llamada a la función client_send(). Esta función requiere los siguientes parámetros: un identificador de websocket válido, un valor de enumeración WINHTTP_WEB_SOCKET_BUFFER_TYPE que especifique el tipo de marco de websocket que se transmitirá, una matriz BYTE que contenga la carga de datos y un argumento ulong que indique el tamaño de la matriz de datos. La función devuelve un valor cero si no se encuentran errores inmediatos; de lo contrario, devuelve un código de error específico.
Internamente, la función WinHttpWebSocketSend() se invoca de forma asincrónica. En consecuencia, el valor de retorno de client_send() representa un estado intermedio, lo que significa la ausencia de errores preliminares durante la configuración de la operación de envío. El resultado de la transmisión de datos real no se devuelve de forma sincrónica. En cambio, el resultado se comunica de forma asincrónica a través de una notificación accesible mediante la función de devolución de llamada registrada. En el contexto de una operación de envío exitosa, se anticipa una notificación WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE. Por el contrario, si ocurre un error durante cualquier operación (enviar o recibir), normalmente se propaga una notificación WINHTTP_CALLBACK_STATUS_REQUEST_ERROR a la función de devolución de llamada.
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;
} Las notificaciones recibidas por la función de devolución de llamada interna se pueden recuperar utilizando la función client_lastcallback_notification(). Esta función devuelve la notificación más reciente recibida por la devolución de llamada para una conexión específica, identificada por el controlador websocket proporcionado como su único argumento. El siguiente fragmento de código ilustra un posible enfoque para gestionar estas notificaciones dentro de un programa MetaTrader 5. Las constantes simbólicas correspondientes a estas notificaciones se definen en el archivo asyncwinhttp.mqh, que se deriva del archivo de encabezado winhttp.h original.
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;
} Para recibir los datos transmitidos por el servidor es necesario colocar inicialmente al cliente en lo que se denomina un estado de sondeo invocando la función client_poll(). Esta acción llama internamente a la función WinHttpWebSocketReceive() dentro de la clase WebSocketClient. De manera similar a la operación de envío, la función WinHTTP se invoca de forma asincrónica, lo que genera el retorno inmediato de un estado intermedio.
La clase WebSocketClient incorpora buffers internos para acomodar los datos sin procesar a su llegada. Una vez que se completa con éxito una operación de lectura, estos datos se ponen en cola dentro de una estructura de datos interna. Este proceso es administrado por el método OnReadComplete() de la clase WebSocketClient. Al finalizar una operación de lectura, el estado de la conexión websocket cambia y deja de "escuchar" activamente los mensajes entrantes.
This implies that an asynchronous read request is not continuous and does not represent persistent polling. Para recuperar mensajes posteriores del servidor, se debe invocar nuevamente la función client_poll(). Básicamente, llamar a client_poll() coloca al cliente websocket en un estado de sondeo temporal sin bloqueo, capturando datos cuando están disponibles y posteriormente activando la notificación WINHTTP_CALLBACK_STATUS_READ_COMPLETE.
El estado actual del cliente websocket se puede consultar llamando a la función client_status(), que devuelve un valor del tipo de enumeración 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;
} La recuperación de datos sin procesar recibidos del servidor se facilita mediante la función client_read(), que acepta los siguientes argumentos:
- un identificador de websocket HINTERNET válido,
- una referencia a una matriz BYTE preasignada, un valor ulong que especifica el tamaño de la matriz mencionada anteriormente,
- una referencia a un valor WINHTTP_WEB_SOCKET_BUFFER_TYPE.
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; }
Los códigos de error se pueden obtener llamando a la función client_lasterror(). La función devuelve un valor DWORD del último error encontrado. Los usuarios también pueden obtener el valor del identificador websocket actual con client_websocket_handle(). Esto podría ser útil cuando se intenta determinar si un identificador se ha cerrado o no.
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;
} La finalización elegante de una conexión a un servidor se inicia invocando la función client_disconnect(). Esta función no devuelve un valor. Sin embargo, inmediatamente cambia el estado del cliente websocket a un estado de cierre. Si posteriormente se recibe un marco de cierre correspondiente desde el servidor, se activará la notificación WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE, modificando así el estado del websocket a cerrado.
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; }
La función final exportada por la DLL es client_reset(). Lo ideal sería llamar a esta función después de desconectarse de un servidor. Su propósito es desasignar los buffers de memoria interna asociados con la conexión cerrada. Si bien no es estrictamente obligatorio, invocar esta función puede ser beneficioso para recuperar recursos de memoria que puedan ser necesarios en otro lugar durante la ejecución del programa. Llamar a client_reset() invalida efectivamente todos los datos asociados con el identificador de websocket especificado, incluidos los códigos de error, los mensajes de error y cualquier dato no leído que quede en la cola interna de marcos.
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
{
if(clients.find(websocket_handle) != clients.end())
{
clients[websocket_handle]->Free();
clients.erase(websocket_handle);
}
} Redefiniendo la clase CWebsocket
Antes de examinar una aplicación MetaTrader 5 que utiliza las funciones detalladas en la sección anterior, se realizará una redefinición de la clase CWebsocket, discutida previamente en el artículo mencionado anteriormente. Esta redefinición reutilizará la clase para aprovechar el cliente websocket asincrónico recientemente desarrollado. El código fuente de esta adaptación se encuentra en el archivo asyncwebsocket.mqh.
La enumeración ENUM_WEBSOCKET_STATE se ha ampliado para incorporar estados adicionales que reflejan la naturaleza asincrónica del cliente. El estado POLLING se activa cuando se inicia una operación de lectura asincrónica. Cuando el socket subyacente recibe datos y los pone a disposición para su recuperación, la función de devolución de llamada señala la finalización de la operación de lectura asincrónica y el estado del cliente websocket pasa a su estado predeterminado: CONNECTED. De manera similar, una operación de envío asincrónica cambia el estado a SENDING. El resultado de esta operación se comunica de forma asincrónica a través de la función de devolución de llamada, por lo que una transmisión exitosa da como resultado un retorno al estado CONNECTED predeterminado.
//+------------------------------------------------------------------+ //| client state enumeration | //+------------------------------------------------------------------+ // client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
Se han integrado varios métodos nuevos en la clase CWebsocket para dar cabida a sus capacidades mejoradas. Los métodos restantes conservan sus firmas originales, y solo se modifican sus implementaciones internas para incorporar la nueva dependencia de DLL. Estos métodos son los siguientes:
Connect(): Este método sirve como punto inicial de interacción para establecer una conexión con un servidor. Acepta los siguientes parámetros:- _serveraddress: La dirección completa del servidor (tipo de datos de cadena).
- _port: El número de puerto del servidor (tipo de datos ushort).
- _secure: Un valor booleano que indica si se debe establecer una conexión segura (tipo de datos booleano).
La implementación de este método se ha simplificado significativamente, ya que la mayor parte de la lógica de establecimiento de la conexión ahora la maneja la DLL subyacente.
//+------------------------------------------------------------------------------------------------------+ //|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); }
Si el método Connect() devuelve un valor booleano verdadero, que indica una conexión exitosa, la transmisión de datos puede comenzar a través del cliente WebSocket. Para este fin se proporcionan dos métodos:
- SendString(): Este método acepta una cadena como entrada.
- Send(): Este método acepta una matriz de caracteres sin signo como su único parámetro.
Ambos métodos devuelven un valor booleano verdadero tras el inicio exitoso de la operación de envío e invocan internamente el método privado clientsend(), que administra todas las operaciones de envío para la clase.
//+------------------------------------------------------------------+ //|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)); }
Al invocar el método Poll() se inicia una operación de lectura asincrónica en el nivel inferior, haciendo que el cliente websocket pase al estado POLLING. Este estado significa que el cliente está esperando una respuesta del servidor.
//+------------------------------------------------------------------+ //| asynchronous read operation (polls for server response) | //+------------------------------------------------------------------+ ulong CWebsocket::Poll(void) { if(hWebSocket!=NULL) return client_poll(hWebSocket); else return WEBSOCKET_ERROR_INVALID_HANDLE; }
Para comprobar si el cliente ha recibido y leído correctamente los datos, el usuario dispone de dos opciones:
- CallBackResult(): Este método verifica la última notificación recibida de la función de devolución de llamada. Una operación de lectura exitosa debería generar una notificación de lectura completa.
- ReadAvailable(): Este método devuelve el tamaño (en bytes) de los datos actualmente disponibles para su recuperación en el búfer interno.
//+------------------------------------------------------------------+ //| 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; }
Luego se puede acceder a los datos sin procesar transmitidos por el servidor utilizando los métodos Read() o ReadString(). Ambos métodos devuelven el tamaño de los datos recibidos. ReadString() requiere una variable de cadena pasada por referencia, en la que se escribirán los datos recibidos, mientras que Read() escribe los datos en una matriz de caracteres sin signo.
//+------------------------------------------------------------------+ //|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); }
Cuando ya no se necesita el cliente WebSocket, la conexión con el servidor se puede finalizar utilizando el método Close(). El método Abort() se diferencia de Close() en que fuerza el cierre de una conexión websocket cerrando los controladores subyacentes, lo que también restablece los valores de las propiedades de clase internas a sus estados predeterminados. Se puede llamar al método explícitamente para realizar la limpieza de recursos.
Finalmente, el método WebSocketHandle() devuelve el identificador del websocket HINTERNET subyacente.
//+------------------------------------------------------------------+ //| 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; }
Usando la DLL
Esta sección presenta un programa ilustrativo diseñado para demostrar la utilización de asyncwinhttpwebsockets.dll. El programa incorpora una interfaz gráfica de usuario (GUI) que establece una conexión con el servicio de eco websocket alojado en https://echo.websocket.org, un recurso específicamente diseñado para probar implementaciones de clientes websocket. La construcción de esta aplicación requiere la biblioteca MQL5 Easy And Fast GUI disponible gratuitamente. El programa se implementa como un Asesor Experto (EA) dentro del entorno MetaTrader 5. La interfaz de usuario cuenta con dos botones que permiten establecer y finalizar una conexión con el servidor designado. Además, se proporciona un campo de entrada de texto para permitir a los usuarios ingresar mensajes para transmitir al servidor.
Las operaciones realizadas por el cliente websocket se registran y se muestran, y cada entrada del registro tiene una marca de tiempo para indicar el momento en que ocurrió. El código fuente de este programa se proporciona a continuación.
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
El gráfico a continuación muestra cómo funciona el programa.

Conclusión
Este artículo detalla el desarrollo de un cliente WebSocket para MetaTrader 5 utilizando la biblioteca WinHTTP en un modo operativo asincrónico. Se construyó una clase dedicada para encapsular esta funcionalidad, y su implementación se demostró dentro de un Asesor Experto (EA) diseñado para interactuar con el servidor de eco alojado en echo.websocket.org. El código fuente completo, incluido el de la biblioteca de vínculos dinámicos, se proporciona en los materiales complementarios. Específicamente, los archivos fuente de C++, junto con un archivo de configuración de compilación CMakeLists.txt, se encuentran dentro del directorio C++ designado. Además, el directorio MQL5 en los materiales complementarios contiene un archivo asyncwinhttpwebsockets.dll precompilado para implementación inmediata.
Para los usuarios que deseen crear la biblioteca de cliente a partir del código fuente proporcionado, el sistema de creación de CMake es requerido. Si CMake está instalado, se puede invocar la interfaz gráfica de usuario (cmake-gui). Luego, el usuario debe especificar el directorio del código fuente, que corresponde a la ubicación del archivo CMakeLists.txt (es decir, MqlWebsocketsClientDLL\Source\C++), y un directorio de compilación separado, que se puede crear en cualquier ubicación deseada.
A continuación, al hacer clic en el botón “Configurar” se iniciará el proceso de configuración. Una ventana de diálogo solicitará al usuario "Especificar el generador para este proyecto", donde se deberá seleccionar la versión adecuada de Visual Studio instalada en el sistema. En la configuración "Plataforma opcional para generador", los usuarios pueden especificar "Win32" para compilar una versión de 32 bits de la DLL; de lo contrario, si se deja este campo en blanco, se obtendrá una compilación predeterminada de 64 bits. Al hacer clic en "Finalizar", CMake procesará la configuración inicial.
Luego aparecerá una notificación de error indicando la necesidad de configurar entradas específicas dentro del archivo CMakeLists.txt. Para solucionar esto, el usuario debe localizar la entrada denominada "ADDITIONAL_LIBRARY_DEPENDENCIES", hacer clic en el campo adyacente y navegar hasta el directorio que contiene el archivo winhttp.lib.
A continuación, el usuario debe localizar la entrada denominada "OUTPUT_DIRECTORY_Xxx_RELEASE" (donde "Xxx" indica la arquitectura, X64 o X86) y establecer la ruta correspondiente a la carpeta "Libraries" de una instalación de MetaTrader.
Después de configurar estas opciones, hacer clic en "Configurar" nuevamente debería completar el proceso de configuración sin más notificaciones de error. El archivo de compilación se puede generar haciendo clic en "Generar". Una generación exitosa activará el botón "Abrir proyecto", que, al hacer clic, abrirá el archivo de proyecto de Visual Studio generado.
Para compilar la DLL, el usuario debe seleccionar "Compilar" y luego "Compilar solución" dentro de Visual Studio. La DLL resultante estará disponible en unos segundos.
| Nombre del archivo o carpeta | Descripción |
|---|---|
| MqlWebsocketsClientDLL\Source\C++ | La carpeta contiene los archivos de código fuente completos para asycnwinhttpwebsockets.dll |
| MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh | Contiene la directiva de importación, que enumera todas las funciones expuestas por asycnwinhttpwebsockets.dll |
| MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh | Contiene la definición de la clase CWebsocket que envuelve la funcionalidad proporcionada por las funciones DLL subyacentes. |
| MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 | El archivo de código para un EA que demuestra la aplicación de la DLL. |
| MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 | El EA compilado que demuestra la aplicación de la DLL. |
| MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll | La DLL compilada que proporciona funcionalidad websocket Winhttp asincrónica. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17877
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Desarrollamos un asesor experto multidivisas (Parte 26): Informador para instrumentos comerciales
Particularidades del trabajo con números del tipo double en MQL4
Pronosticamos barras Renko con ayuda de IA CatBoost
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Podría ayudar ver algún código de ejemplo en:
La misma premisa básica se aplica a un EA.Es una idea excelente.
Les agradezco a ambos por su respuesta a mi pregunta. Debo haber pasado por alto las definiciones de funciones sobrecargadas y sólo leí sobre la primera. ¿Saben si la Terminal es lo suficientemente inteligente como para procesar en paralelo las llamadas iCustom para maximizar la utilización del procesador ya que planeo variar el parámetro de símbolo para cada uno de los 28 pares y planeo tener múltiples llamadas iCustom como la Brooky Trend Strength.
Además, ¿podría alguno de ustedes decirme dónde puedo publicar comentarios sobre errores en MQ5 y también donde sugerencias para los administradores de Mq. He encontrado algunos, más recientemente la diferencia Bars entre el terminal y el probador de estrategia. Además, tengo una configuración de 3 screem con la pantalla principal en el extremo izquierdo. Tratando de mover un panel. El puntero del ratón de arrastre está en la pantalla más a la izquierda, pero el panel de arrastre está en el centro. Creo que el Terminal o Windows se está volviendo loco cuando el ratón se mueve un píxel y luego cambia de pantalla para mover el panel de un píxel y otra vez.
Bars() se hipo donde hay datos de precios que faltan - donde rates_total no lo hará. Si no recuerdo mal lo que leí en el pasado, Bars() se puede arreglar haciendo referencia a las marcas de tiempo. Podría valer la pena una búsqueda.
Tengo una configuración de 3 pantallas con la pantalla principal en el extremo izquierdo. Tratar de mover un panel, como el Navegador o los paneles de Mercado, de la izquierda a la derecha es muy tedioso. El puntero del ratón de arrastre está en la pantalla más a la izquierda, pero el panel de arrastre está en el medio. Creo que el Terminal o Windows se está volviendo loco cuando el ratón se mueve un píxel y luego cambia de pantalla para mover el panel de un píxel y volver de nuevo.
Realmente no sé en este caso. Tengo 3 ordenadores, cada uno con su propio monitor y terminal. Sé que Windows generalmente tiene configuraciones de visualización de múltiples monitores, incluyendo imagen-en-imagen tal vez como una solución.
¿Puede alguien más con múltiples monitores reales en una sola máquina intervenir aquí, por favor?
¡¡¡Gran Información!!!
Gracias Ryan, tu comentario respecto a bars vs rates_total es apropiado. Mi problema es que los dos son idénticos en Terminal pero en el STrategy Tester Visualize, Bars es uno mayor lo que me llevó a mi bobo por no leer la documentación hasta el final. Voy a tomar tu aportación y usarla para iCustom. Supongo que debe haber una dirección iCustom distinta para cada combinación de Symbol y Time specifications.
Además, ¿hay alguna manera de que un EA muestre Texto en la pantalla en el Probador de Estrategias? En Mq4 lo hacía automáticamente pero ahora no. Uso muchos objetos de clase para mostrar información y poner una segunda copia en la plantilla ralentiza aún más el Probador de Estrategias.
Sobre la pantalla de 3 paneles, creo que el problema es que el terminal no actualiza correctamente la ubicación del monitor cuando el ratón se mueve de la pantalla 2 a la pantalla 1.
Yo tengo 2 mini pcs que soportan 3 monitores cada uno así que tengo las 3 pantallas conectadas a ambos minis y uso HDMI1 para un pc y HDMI2 para el otro. Funciona de maravilla con Fire Tvs de 43" aunque hay que asegurarse de que los mandos están bien configurados para controlar un solo monitor (llamar al soporte de amazon). El único inconveniente es que el botón de encendido y apagado apaga todos los monitores y a veces tengo que desenchufar para sincronizar la alimentación.
CapeCoddah
Además, ¿hay alguna manera de que un EA muestre texto en la pantalla en el Probador de Estrategias? En Mq4 lo hacía automáticamente pero ahora no. Utilizo muchos objetos de clase para mostrar información y poner una segunda copia en la plantilla ralentiza aún más el Probador de Estrategias.
En la pantalla de 3 paneles, creo que el problema es que el terminal no actualiza correctamente la ubicación del monitor cuando el ratón se mueve de la pantalla 2 a la pantalla 1.