WebSocket для MetaTrader 5 — Асинхронные клиентские соединения с помощью Windows API
Введение
В статье «WebSocket для MetaTrader 5: использование Windows API» показано использование Windows API для реализации клиента WebSocket в приложениях MetaTrader 5. Представленная там реализация была ограничена синхронным режимом работы.
В настоящей статье мы вновь рассмотрим применение Windows API для создания клиента WebSocket для программ MetaTrader 5 с целью достижения асинхронной клиентской функциональности. Практическая методология реализации этой цели предполагает создание пользовательской динамически подключаемой библиотеки (DLL), экспортирующей функции, подходящие для интеграции с приложениями MetaTrader 5.
Соответственно, в настоящей статье будет рассмотрен процесс разработки DLL, а затем представлена демонстрация ее применения на примере программы MetaTrader 5.
Асинхронный режим WinHTTP
Предпосылки для асинхронной работы в библиотеке WinHTTP, как указано в ее документации, двоякие. Во-первых, во время вызова функции WinHTTPOpen хэндл сессии должен быть настроен с помощью флага WINHTTP_FLAG_ASYNC или WINHTTP_FLAG_SECURE_DEFAULTS.
// Set hSession hSession = WinHttpOpen(L"MyApp", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); if (hSession == NULL) ErrorCode = ERROR_INVALID_HANDLE; // Return error code return ErrorCode;
После создания действительного хэндла сессии пользователи должны зарегистрировать функцию обратного вызова, чтобы получать уведомления о различных событиях, связанных с определенными вызовами функции WinHTTP. Эта функция обратного вызова статуса информируется о ходе асинхронных операций с помощью флагов уведомлений.
Регистрация функции обратного вызова осуществляется через функцию WinHttpSetStatusCallback, которая также позволяет указывать флаги уведомлений, которыми будет управлять обратный вызов. Пользователи могут подписаться на полный набор уведомлений или на более ограниченное подмножество. Кроме того, отдельные функции обратного вызова могут быть назначены для хэндлов сессий, запросов и WebSocket соответственно.
Важно отметить, что регистрация функции обратного вызова сразу после создания хэндла сессии не является обязательной; функция WinHttpSetStatusCallback может быть вызвана для любого действительного хэндла HINTERNET на любом этапе до или во время инициализации соединения веб-сокет.
if (!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this))) { // Handle error ErrorCode = GetLastError(); return ErrorCode; } if (WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK) { ErrorCode = GetLastError(); return ErrorCode; }
Сигнатура определяемой пользователем функции обратного вызова включает параметр, указывающий на определенную пользователем структуру данных, обычно называемую значением контекста. Этот механизм облегчает передачу данных из функции обратного вызова.
Значение контекста должно быть указано перед регистрацией функции обратного вызова посредством вызова функции WinHttpSetOption с флагом опции WINHTTP_OPTION_CONTEXT_VALUE. Уместно признать, что при эмпирическом тестировании возникли трудности с надежным получением зарегистрированного значения контекста с помощью этого метода.
Хотя нельзя полностью сбрасывать со счетов возможность ошибки реализации, последовательный сбой потребовал использования глобальной переменной в качестве альтернативы, и эта деталь будет рассмотрена при последующем обсуждении реализации библиотеки DLL.
Наконец, важным соображением, касающимся определяемой пользователем функции обратного вызова, является требование потокобезопасности. Однако, учитывая, что данная работа предполагает создание библиотеки DLL для использования в среде MetaTrader 5, это ограничение может быть снято. Это связано с однопоточным характером программ MetaTrader 5 в целом, и, хотя DLL-код выполняется в пуле потоков процесса загрузки, в программах MetaTrader 5, активен только один поток.
void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) Реализация библиотеки DLL
Библиотека DLL создается в среде Visual Studio с использованием языка программирования C++. Этот процесс требует установки рабочей нагрузки "C++ Desktop Development" в Visual Studio вместе с комплектом разработки программного обеспечения (SDK) для Windows 10 или Windows 11. Пакет SDK для Windows является обязательным условием, поскольку он предоставляет файл библиотеки WinHTTP (.lib), с которым будет связана библиотека DLL во время компиляции. Результирующая библиотека DLL содержит, по крайней мере, три основных компонента.
Первый - это класс, который инкапсулирует основные функциональные возможности клиента WinHTTP WebSocket. Второй - это отдельная функция обратного вызова, которая работает совместно с глобальной переменной, облегчая манипулирование соединением WebSocket как внутри области действия функции обратного вызова, так и за ее пределами. Третий компонент состоит из набора упрощенных функций-обёрток, которые будут предоставлены библиотекой DLL для использования в программах MetaTrader 5. Реализация начинается с кода, определенного в заголовочном файле asyncwebsocketclient.h.
Этот заголовочный файл начинается с объявления класса WebSocketClient, где каждый экземпляр представляет отдельное клиентское соединение.
// WebSocket client class class WebSocketClient { private: // Application session handle to use with this connection HINTERNET hSession; // Windows connect handle HINTERNET hConnect; // The initial HTTP request handle to start the WebSocket handshake HINTERNET hRequest; // Windows WebSocket handle HINTERNET hWebSocket; //initialization flag DWORD initialized; //sent bytes DWORD bytesTX; //last error code DWORD ErrorCode; //last completed websocket operation as indicated by callback function DWORD completed_websocket_operation; //internal queue of frames sent from a server std::queue<Frame>* frames; //client state; ENUM_WEBSOCKET_STATE status; //sets an hSession handle DWORD Initialize(VOID); // reset state of object /* reset_error: boolean flag indicating whether to rest the internal error buffers. */ VOID Reset(bool reset_error = true); public: //constructor(s) WebSocketClient(VOID); WebSocketClient(const WebSocketClient&) = delete; WebSocketClient(WebSocketClient&&) = delete; WebSocketClient& operator=(const WebSocketClient&) = delete; WebSocketClient& operator=(WebSocketClient&&) = delete; //destructor ~WebSocketClient(VOID); //received bytes; DWORD bytesRX; // receive buffer std::vector<BYTE> rxBuffer; // received frame type; WINHTTP_WEB_SOCKET_BUFFER_TYPE rxBufferType; // Get the winhttp websocket handle /* return: returns the hWebSocket handle which is used to identify a websocket connection instance */ HINTERNET WebSocketHandle(VOID); // Connect to a server /* hsession: HINTERNET session handle host: is the url port: prefered port number to use secure: 0 is false, non-zero is true return: DWORD error code, 0 indicates success and non-zero for failure */ DWORD Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure); // Send data to the WebSocket server /* bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type pBuffer: pointer to the data to be sent dwLength: size of pBuffer data return: DWORD error code, 0 indicates success and non-zero for failure */ DWORD Send(WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType, void* pBuffer, DWORD dwLength); // Close the connection to the server /* status: WINHTTP_WEB_SOCKET_CLOSE_STATUS enumeration of the close notification to be sent reason: character string of extra data sent with the close notification return: DWORD error code, 0 indicates success and non-zero for failure */ DWORD Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS status, CHAR* reason = NULL); // Retrieve the close status sent by a server /* pusStatus: pointer to a close status code that will be filled upon return. pvReason: pointer to a buffer that will receive a close reason dwReasonLength: The length of the pvReason buffer, pdwReasonLengthConsumed:The number of bytes consumed. If pvReason is NULL and dwReasonLength is 0, pdwReasonLengthConsumed will contain the size of the buffer that needs to be allocated by the calling application. return: DWORD error code, 0 indicates success and non-zero for failure */ DWORD QueryCloseStatus(USHORT* pusStatus, PVOID pvReason, DWORD dwReasonLength, DWORD* pdwReasonLengthConsumed); // read from the server /* bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type pBuffer: pointer to the data to be sent pLength: size of pBuffer bytesRead: pointer to number bytes read from the server pBufferType: pointer to type of frame sent from the server return: DWORD error code, 0 indicates success and non-zero for failure */ DWORD Receive(PVOID pBuffer, DWORD pLength, DWORD* bytesRead, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType); // Check client state /* return: ENUM_WEBSOCKET_STATE enumeration */ ENUM_WEBSOCKET_STATE Status(VOID); // get frames cached in the internal queue /* pBuffer: User supplied container to which data is written to pLength: size of pBuffer pBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type */ VOID Read(BYTE* pBuffer, DWORD pLength, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType); // get bytes received /* return: Size of most recently cached frame sent from a server */ DWORD ReadAvailable(VOID); // get the last error /* return: returns the last error code */ DWORD LastError(VOID); // activate callback function /* return: DWORD error code, 0 indicates success and non-zero for failure */ DWORD EnableCallBack(VOID); // set error /* message: Error description to be captured errorcode: new user defined error code */ VOID SetError(const DWORD errorcode); // get the last completed operation /* returns: DWORD constant of last websocket operation */ DWORD LastOperation(VOID); //deinitialize the session handle and free up resources VOID Free(VOID); //the following methods define handlers meant to be triggered by the callback function// // on error /* result: pointer to WINHTTP_ASYNC_RESULT structure that encapsulates the specific event that triggered the error */ VOID OnError(const WINHTTP_ASYNC_RESULT* result); // read completion handler /* read: Number of bytes of data successfully read from the server buffertype: type of frame read-in. Called when successfull read is completed */ VOID OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype); // websocket close handler /* Handles the a successfull close request */ VOID OnClose(VOID); // Send operation handler /* sent: the number of bytes successfully sent to the server if any This is a handler for an asynchronous send that interacts with the callback function */ VOID OnSendComplete(const DWORD sent); //set the last completed websocket operation /* operation : constant defining the operation flagged as completed by callback function */ VOID OnCallBack(const DWORD operation); };
Наряду с классом для представления кадра сообщения определяется структура, Frame.
struct Frame { std::vector<BYTE>frame_buffer; WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type; DWORD frame_size; };
Далее, для описания различных состояний соединения WebSocket объявлено перечисление ENUM_WEBSOCKET_STATE.
// client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
Далее, asyncwebsocketclient.h объявляет глобальную переменную с именем clients. Эта переменная представляет собой контейнер, в частности, карту, предназначенную для хранения активных подсоединений WebSocket. Глобальный масштаб этого контейнера map обеспечивает его доступность для любой функции обратного вызова, определенной в библиотеке.
// container for websocket objects accessible to callback function extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;
Файл asyncwebsocketclient.h завершается определением набора функций, определяемых интерфейсом WEBSOCK_API. Этот определитель служит для отметки этих функций для экспорта библиотекой DLL. Эти функции составляют вышеупомянутые функции-обёртки и представляют собой интерфейс, через который разработчики будут взаимодействовать с библиотекой DLL в своих приложениях 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);
Изучение определения функции WebSocketCallback() показывает использование глобальной переменной clients для управления уведомлениями. Текущая реализация настроена на обработку так называемых уведомлений о завершении в документации WinHTTP. Эти уведомления активируются после успешного завершения любой асинхронной операции. Например, WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE сигнализирует о завершении операции отправки, а WINHTTP_CALLBACK_STATUS_READ_COMPLETE указывает на завершение операции чтения.
void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) { if (WinHttpWebSocketClient::clients.find(hInternet)!= WinHttpWebSocketClient::clients.end()) { WinHttpWebSocketClient::clients[hInternet]->OnCallBack(dwInternetStatus); switch (dwInternetStatus) { case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE: WinHttpWebSocketClient::clients[hInternet]->OnClose(); break; case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: WinHttpWebSocketClient::clients[hInternet]->OnSendComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred); break; case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: WinHttpWebSocketClient::clients[hInternet]->OnReadComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred, ((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->eBufferType); break; case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: WinHttpWebSocketClient::clients[hInternet]->OnError((WINHTTP_ASYNC_RESULT*)lpvStatusInformation); break; default: break; } } }
Аргумент hInternet функции обратного вызова служит хэндлом HINTERNET, по которому обратный вызов был первоначально зарегистрирован. Этот хэндл используется для индексации члена в глобальном контейнере map, clients, возвращая указатель на экземпляр WebSocketClient. Конкретное уведомление об обратном вызове передается через аргумент dwInternetStatus. Примечательно, что данные, представленные аргументами lpvStatusInformation и dwStatusInformationLength, меняются в зависимости от значения dwInternetStatus.
Например, когда dwInternetStatus принимает значения WINHTTP_CALLBACK_STATUS_READ_COMPLETE или WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE, параметр lpvStatusInformation содержит указатель на структуру WINHTTP_WEB_SOCKET_STATUS, а dwStatusInformationLength указывает размер справочных данных.
Наша реализация выборочно обрабатывает подмножество уведомлений, предоставляемых этой функцией обратного вызова, каждое из которых приводит к изменению состояния соответствующего экземпляра WebSocketClient. В частности, метод OnCallBack() фиксирует коды состояния, связанные с этими уведомлениями. Эта информация хранится в экземпляре WebSocketClient, где она может быть доступна пользователям с помощью функции-обёртки.
VOID WebSocketClient::OnCallBack(const DWORD operation)
{
completed_websocket_operation = operation;
} Метод OnReadComplete() в классе WebSocketClient отвечает за передачу необработанных данных в буфер фреймов в очереди, из которого пользователи впоследствии могут запрашивать доступность данных для извлечения.
VOID WebSocketClient::OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype) { bytesRX = read; rxBufferType = buffertype; status = ENUM_WEBSOCKET_STATE::CONNECTED; Frame frame; frame.frame_buffer.insert(frame.frame_buffer.begin(), rxBuffer.data(), rxBuffer.data() + read); frame.frame_type = buffertype; frame.frame_size = read; frames->push(frame); }
Метод OnSendComplete() обновляет внутренние поля, которые указывают на успешную операцию отправки, что также вызывает изменение состояния клиента WebSocket.
VOID WebSocketClient::OnSendComplete(const DWORD sent) { bytesTX = sent; status = ENUM_WEBSOCKET_STATE::CONNECTED; return; }
Наконец, метод onError() фиксирует любую информацию, связанную с ошибкой, предоставленную с помощью аргумента lpvStatusInformation.
VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result) { SetError(result->dwError); Reset(false); }
Установление соединения с сервером осуществляется с помощью функции client_connect(). Эта функция вызывается с адресом сервера (в виде строки), номером порта (в виде целого числа), логическим значением, указывающим, должно ли соединение быть безопасным, и указателем на значение в HINTERNET. Функция возвращает значение DWORD, а также задает аргумент HINTERNET. В случае возникновения какой-либо ошибки в процессе соединения функция установит для аргумента HINTERNET значение NULL и вернет ненулевой код ошибки.
Внутренняя функция client_connect() инициализирует экземпляр класса WebSocketClient и использует его для установления соединения. После успешного установления соединения указатель на этот экземпляр WebSocketClient сохраняется в глобальном контейнере clients, а хэндл WebSocket экземпляра служит уникальным ключом. Этот хэндл WebSocket используется для уникальной идентификации конкретного соединения WebSocket как во внутренних операциях библиотеки DLL, так и во внешних, выполняемых вызывающей программой MetaTrader 5.
DWORD WEBSOCK_API client_connect( const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocketp_handle) { DWORD errorCode = 0; auto client = std::make_shared<WebSocketClient>(); if (client->Connect(url, port, secure) != NO_ERROR) errorCode = client->LastError(); else { HINTERNET handle = client->WebSocketHandle(); if (client->EnableCallBack()) { errorCode = client->LastError(); client->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS); client->Free(); handle = NULL; } else { clients[handle] = client; *websocketp_handle = handle; } } return errorCode; }
Инициализацией и установлением соединений WebSocket управляет метод Connect() класса WebSocketClient. Процесс подключения изначально выполняется синхронно. После этого функция обратного вызова регистрируется в хэндле WebSocket посредством вызова метода EnableCallBack(), что позволяет получать уведомления об асинхронных событиях.
DWORD WebSocketClient::Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure) { if((status != ENUM_WEBSOCKET_STATE::CLOSED)) { ErrorCode = Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,NULL); return WEBSOCKET_ERROR_CLOSING_ACTIVE_CONNECTION; } status = ENUM_WEBSOCKET_STATE::CONNECTING; // Return 0 for success if(hSession == NULL) { ErrorCode = Initialize(); if(ErrorCode) { Reset(false); return ErrorCode; } } // Cracked URL variable pointers URL_COMPONENTS UrlComponents; // Create cracked URL buffer variables std::unique_ptr <WCHAR> scheme(new WCHAR[0x20]); std::unique_ptr <WCHAR> hostName(new WCHAR[0x100]); std::unique_ptr <WCHAR> urlPath(new WCHAR[0x1000]); DWORD dwFlags = 0; if(secure) dwFlags |= WINHTTP_FLAG_SECURE; if(scheme == NULL || hostName == NULL || urlPath == NULL) { ErrorCode = ERROR_NOT_ENOUGH_MEMORY; Reset(); return ErrorCode; } // Clear error's ErrorCode = 0; // Setup UrlComponents structure memset(&UrlComponents, 0, sizeof(URL_COMPONENTS)); UrlComponents.dwStructSize = sizeof(URL_COMPONENTS); UrlComponents.dwSchemeLength = -1; UrlComponents.dwHostNameLength = -1; UrlComponents.dwUserNameLength = -1; UrlComponents.dwPasswordLength = -1; UrlComponents.dwUrlPathLength = -1; UrlComponents.dwExtraInfoLength = -1; // Get the individual parts of the url if(!WinHttpCrackUrl(host, NULL, 0, &UrlComponents)) { // Handle error ErrorCode = GetLastError(); Reset(); return ErrorCode; } // Copy cracked URL hostName & UrlPath to buffers so they are separated if(wcsncpy_s(scheme.get(), 0x20, UrlComponents.lpszScheme, UrlComponents.dwSchemeLength) != 0 || wcsncpy_s(hostName.get(), 0x100, UrlComponents.lpszHostName, UrlComponents.dwHostNameLength) != 0 || wcsncpy_s(urlPath.get(), 0x1000, UrlComponents.lpszUrlPath, UrlComponents.dwUrlPathLength) != 0) { ErrorCode = GetLastError(); Reset(false); return ErrorCode; } if(port == 0) { if((_wcsicmp(scheme.get(), L"wss") == 0) || (_wcsicmp(scheme.get(), L"https") == 0)) { UrlComponents.nPort = INTERNET_DEFAULT_HTTPS_PORT; } else if((_wcsicmp(scheme.get(), L"ws") == 0) || (_wcsicmp(scheme.get(), L"http")) == 0) { UrlComponents.nPort = INTERNET_DEFAULT_HTTP_PORT; } else { ErrorCode = ERROR_INVALID_PARAMETER; Reset(false); return ErrorCode; } } else UrlComponents.nPort = port; // Call the WinHttp Connect method hConnect = WinHttpConnect(hSession, hostName.get(), UrlComponents.nPort, 0); if(!hConnect) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } // Create a HTTP request hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.get(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags); if(!hRequest) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } // Set option for client certificate if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, 0)) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } // Add WebSocket upgrade to our HTTP request #pragma prefast(suppress:6387, "WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET does not take any arguments.") if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, 0, 0)) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } // Send the WebSocket upgrade request. if(!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, 0, 0, 0, 0)) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } // Receive response from the server if(!WinHttpReceiveResponse(hRequest, 0)) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } // Finally complete the upgrade hWebSocket = WinHttpWebSocketCompleteUpgrade(hRequest, NULL); if(hWebSocket == 0) { // Handle error ErrorCode = GetLastError(); Reset(false); return ErrorCode; } status = ENUM_WEBSOCKET_STATE::CONNECTED; // Return should be zero return ErrorCode; } DWORD WebSocketClient::EnableCallBack(VOID) { if(!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this))) { // Handle error ErrorCode = GetLastError(); return ErrorCode; } if(WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK) { ErrorCode = GetLastError(); return ErrorCode; } return ErrorCode; }
После установления соединения с сервером и получения действительного хэндла пользователи могут инициировать взаимодействие с удаленной конечной точкой. Передача данных на сервер осуществляется посредством вызова функции client_send(). Для этой функции требуются следующие параметры: допустимый хэндл WebSocket, значение перечисления WINHTTP_WEB_SOCKET_BUFFER_TYPE, указывающее тип передаваемого кадра WebSocket, массив BYTE, содержащий полезную нагрузку данных, и аргумент ulong, указывающий размер массива данных. Функция возвращает нулевое значение, если никаких непосредственных ошибок обнаружено не было; в противном случае она возвращает определенный код ошибки.
Внутренняя функция WinHttpWebSocketSend() вызывается асинхронно. Следовательно, возвращаемое значение client_send() представляет промежуточный статус, означающий отсутствие предварительных ошибок во время настройки операции отправки. Результат фактической передачи данных возвращается не синхронно. Напротив, результат передается асинхронно через уведомление, доступное через зарегистрированную функцию обратного вызова. В контексте успешной операции отправки ожидается уведомление WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE. И наоборот, если во время какой-либо операции (отправки или получения) возникает ошибка, в функцию обратного вызова обычно передается уведомление WINHTTP_CALLBACK_STATUS_REQUEST_ERROR.
DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length)
{
DWORD out = 0;
if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
out = WEBSOCKET_ERROR_INVALID_HANDLE;
else
out = clients[websocket_handle]->Send(buffertype, message, length);
return out;
} Уведомления, полученные внутренней функцией обратного вызова, можно получить с помощью функции client_lastcallback_notification(). Эта функция возвращает самое последнее уведомление, полученное обратным вызовом для определенного соединения, идентифицируемое хэндлом WebSocket, предоставленным в качестве единственного аргумента. Последующий фрагмент кода иллюстрирует потенциальный подход к обработке этих уведомлений в программе MetaTrader 5. Символические константы, соответствующие этим уведомлениям, определены в файле asyncwinhttp.mqh, который является производным от исходного файла заголовков winhttp.h.
DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle)
{
DWORD out = 0;
if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
out = WEBSOCKET_ERROR_INVALID_HANDLE;
else
out = clients[websocket_handle]->LastOperation();
return out;
} Для получения данных, передаваемых сервером, первоначально требуется перевести клиента в состояние, называемое состоянием опроса, путем вызова функции client_poll(). Это действие вызывает внутреннюю функцию WinHttpWebSocketReceive() в классе WebSocketClient. Аналогично операции отправки, функция WinHTTP вызывается асинхронно, что приводит к немедленному возврату промежуточного состояния.
Класс WebSocketClient включает в себя внутренние буферы для размещения необработанных данных по их поступлении. Как только операция чтения успешно завершена, эти данные помещаются в очередь во внутренней структуре данных. Этим процессом управляет метод OnReadComplete() класса WebSocketClient. По завершении операции чтения состояние соединения WebSocket меняется, и оно перестает активно "прослушивать" входящие сообщения.
Это означает, что асинхронный запрос на чтение не является непрерывным и не представляет собой постоянный опрос. Для получения последующих сообщений с сервера необходимо снова вызвать функцию client_poll(). По сути, вызов client_poll() переводит клиент WebSocket во временное, неблокирующее состояние опроса, захватывая данные, когда они становятся доступными, и впоследствии запуская уведомление WINHTTP_CALLBACK_STATUS_READ_COMPLETE.
Текущее состояние клиента WebSocket можно запросить, вызвав функцию client_status(), которая возвращает значение типа перечисления ENUM_WEBSOCKET_STATE.
DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle)
{
DWORD out = 0;
if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
out = WEBSOCKET_ERROR_INVALID_HANDLE;
else
out = clients[websocket_handle]->Receive(clients[websocket_handle]->rxBuffer.data(), (DWORD)clients[websocket_handle]->rxBuffer.size(), &clients[websocket_handle]->bytesRX, &clients[websocket_handle]->rxBufferType);
return out;
}
ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle)
{
ENUM_WEBSOCKET_STATE out = {};
if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
out = {};
else
out = clients[websocket_handle]->Status();
return out;
} Поиск необработанных данных, полученных с сервера, облегчается функцией client_read(), принимающей следующие аргументы:
- действительный хэндл WebSocket HINTERNET,
- ссылка на предварительно выделенный массив BYTE, значение ulong, определяющее размер вышеупомянутого массива,
- ссылка на значение 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; }
Коды ошибок можно получить, вызвав функцию client_lasterror(). Функция возвращает значение DWORD для последней обнаруженной ошибки. Пользователи также могут получить текущее значение хэндла WebSocket с помощью client_websocket_handle(). Это может быть полезно при попытке определить, был ли хэндл закрыт или нет.
DWORD WEBSOCK_API client_lasterror(HINTERNET websocket_handle)
{
DWORD out = 0;
if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
out = WEBSOCKET_ERROR_INVALID_HANDLE;
else
out = clients[websocket_handle]->LastError();
return out;
}
HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle)
{
HINTERNET out = NULL;
if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
out = NULL;
else
out = clients[websocket_handle]->WebSocketHandle();
return out;
} Плавное завершение соединения с сервером инициируется вызовом функции client_disconnect(). Эта функция не возвращает значение. Однако немедленно переводит состояние клиента WebSocket в состояние закрытия. Если впоследствии с сервера будет получен соответствующий кадр закрытия, будет запущено уведомление WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE, тем самым изменяя состояние WebSocket на закрытое.
void WEBSOCK_API client_disconnect(HINTERNET websocket_handle) { if(clients.find(websocket_handle) != clients.end()) { if(clients[websocket_handle]->WebSocketHandle() != NULL) { clients[websocket_handle]->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS); } } return; }
Последней функцией, экспортируемой библиотекой DLL, является client_reset(). В идеале эта функция должна вызываться после отключения от сервера. Его цель - освободить буферы внутренней памяти, связанные с закрытым соединением. Хотя использование этой функции и не является строго обязательным, она может быть полезна для высвобождения ресурсов памяти, которые могут потребоваться в других местах во время выполнения программы. Вызов функции client_reset() фактически делает недействительными все данные, связанные с указанным хэндлом WebSocket, включая коды ошибок, сообщения об ошибках и любые непрочитанные данные, оставшиеся во внутренней очереди кадров.
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
{
if(clients.find(websocket_handle) != clients.end())
{
clients[websocket_handle]->Free();
clients.erase(websocket_handle);
}
} Переопределение класса CWebsocket
Прежде чем приступить к изучению приложения MetaTrader 5, использующего функции, описанные в предыдущем разделе, мы проведем переопределение класса CWebsocket, которое ранее обсуждалось в вышеупомянутой статье. Это переопределение позволит перепрофилировать класс для использования с недавно разработанным асинхронным клиентом WebSocket. Исходный код для этой адаптации находится в файле asyncwebsocket.mqh. П
еречисление ENUM_WEBSOCKET_STATE было расширено, чтобы включить дополнительные состояния, отражающие асинхронную природу клиента. Состояние опроса (POLLING) включается при запуске операции асинхронного чтения. После того, как базовый сокет получает данные и делает их доступными для извлечения, функция обратного вызова сигнализирует о завершении операции асинхронного чтения, и состояние клиента WebSocket переходит в состояние по умолчанию: СОЕДИНЕНО (CONNECTED). Аналогично, операция асинхронной отправки переводит состояние в режим ОТПРАВКИ (SENDING). Результат этой операции передается асинхронно через функцию обратного вызова, в результате чего успешная передача приводит к возврату в состояние СОЕДИНЕНИЯ по умолчанию.
//+------------------------------------------------------------------+ //| client state enumeration | //+------------------------------------------------------------------+ // client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
В класс CWebsocket интегрировано несколько новых методов для реализации его расширенных возможностей. Остальные методы сохраняют свои исходные сигнатуры, и только их внутренние реализации изменены для включения новой зависимости DLL. Вот эти методы:
Connect(): Этот метод служит начальной точкой взаимодействия для установления соединения с сервером. Он принимает следующие параметры:- _serveraddress: Полный адрес сервера (строковый тип данных).
- _port: Номер порта сервера (тип данных ushort).
- _secure: Логическое значение, указывающее, следует ли устанавливать безопасное соединение (логический тип данных).
Реализация этого метода была значительно упрощена, так как большая часть логики установления соединения теперь обрабатывается базовой библиотекой DLL.
//+------------------------------------------------------------------------------------------------------+ //|Connect method used to set server parameters and establish client connection | //+------------------------------------------------------------------------------------------------------+ bool CWebsocket::Connect(const string _serveraddress,const INTERNET_PORT port=443, bool secure = true) { if(initialized) { if(StringCompare(_serveraddress,serveraddress,false)) Close(); else return(true); } serveraddress = _serveraddress; int dot=StringFind(serveraddress,"."); int ss=(dot>0)?StringFind(serveraddress,"/",dot):-1; serverPath=(ss>0)?StringSubstr(serveraddress,ss+1):"/"; int sss=StringFind(serveraddress,"://"); if(sss<0) sss=-3; serverName=StringSubstr(serveraddress,sss+3,ss-(sss+3)); serverPort=port; DWORD connect_error = client_connect(serveraddress,port,ulong(secure),hWebSocket); if(hWebSocket<=0) { Print(__FUNCTION__," Connection to ", serveraddress, " failed. \n", GetErrorDescription(connect_error)); return(false); } else initialized = true; return(true); }
Если метод Connect() возвращает логическое значение true, указывающее на успешное соединение, передача данных может начаться через клиент WebSocket. Для этой цели предусмотрены два метода:
- SendString(): Этот метод принимает строку в качестве входных данных.
- Send(): Этот метод принимает массив беззнаковых символов в качестве единственного параметра.
Оба метода возвращают логическое значение true при успешном запуске операции отправки и внутренне вызывают приватный метод clientsend(), который управляет всеми операциями отправки для класса.
//+------------------------------------------------------------------+ //|public method for sending raw string messages | //+------------------------------------------------------------------+ bool CWebsocket::SendString(const string msg) { if(!initialized || hWebSocket == NULL) { Print(__FUNCTION__, " No websocket connection "); return(false); } if(StringLen(msg)<=0) { Print(__FUNCTION__, " Message buffer is empty "); return(false); } BYTE msg_array[]; StringToCharArray(msg,msg_array,0,WHOLE_ARRAY); ArrayRemove(msg_array,ArraySize(msg_array)-1,1); DWORD len=(ArraySize(msg_array)); return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE)); } //+------------------------------------------------------------------+ //|Public method for sending data prepackaged in an array | //+------------------------------------------------------------------+ bool CWebsocket::Send(BYTE &buffer[]) { if(!initialized || hWebSocket == NULL) { Print(__FUNCTION__, " No websocket connection "); return(false); } return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE)); }
Вызов метода Poll() инициирует операцию асинхронного чтения на более низком уровне, переводя клиент WebSocket в состояние ОПРОСА (POLLING). Это состояние означает, что клиент ожидает ответа от сервера.
//+------------------------------------------------------------------+ //| asynchronous read operation (polls for server response) | //+------------------------------------------------------------------+ ulong CWebsocket::Poll(void) { if(hWebSocket!=NULL) return client_poll(hWebSocket); else return WEBSOCKET_ERROR_INVALID_HANDLE; }
Чтобы убедиться, что данные были получены и успешно считаны клиентом, пользователю доступны два варианта:
- CallBackResult(): Этот метод проверяет последнее уведомление, полученное от функции обратного вызова. Результатом успешной операции чтения должно стать уведомление о завершении чтения.
- ReadAvailable(): Этот метод возвращает размер (в байтах) данных, доступных в данный момент для извлечения из внутреннего буфера.
//+------------------------------------------------------------------+ //| call back notification | //+------------------------------------------------------------------+ ulong CWebsocket::CallBackResult(void) { if(hWebSocket!=NULL) return client_lastcallback_notification(hWebSocket); else return WINHTTP_CALLBACK_STATUS_DEFAULT; } //+------------------------------------------------------------------+ //| check if any data has read from the server | //+------------------------------------------------------------------+ ulong CWebsocket::ReadAvailable(void) { if(hWebSocket!=NULL) return(client_readable(hWebSocket)); else return 0; }
Затем к необработанным данным, передаваемым сервером, можно получить доступ с помощью методов Read() или ReadString(). Оба метода возвращают размер получаемых данных. Для функции ReadString() требуется передаваемая по ссылке строковая переменная, в которую будут записаны полученные данные, тогда как функция Read() записывает данные в массив беззнаковых символов.
//+------------------------------------------------------------------+ //|public method for reading data sent from the server | //+------------------------------------------------------------------+ ulong CWebsocket::Read(BYTE &buffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE &buffertype) { if(!initialized || hWebSocket == NULL) { Print(__FUNCTION__, " No websocket connection "); return(false); } ulong bytes_read_from_socket=0; clientread(buffer,buffertype,bytes_read_from_socket); return(bytes_read_from_socket); } //+------------------------------------------------------------------+ //|public method for reading data sent from the server | //+------------------------------------------------------------------+ ulong CWebsocket::ReadString(string &_response) { if(!initialized || hWebSocket == NULL) { Print(__FUNCTION__, " No websocket connection "); return(false); } ulong bytes_read_from_socket=0; ZeroMemory(rxbuffer); WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype; clientread(rxbuffer,rbuffertype,bytes_read_from_socket); _response=(bytes_read_from_socket)?CharArrayToString(rxbuffer):""; return(bytes_read_from_socket); }
Когда клиент WebSocket больше не требуется, соединение с сервером может быть прервано с помощью метода Close(). Метод Abort() отличается от метода Close() тем, что он принудительно закрывает соединение WebSocket путем закрытия базовых хэндлов, что также возвращает значения внутренних свойств класса к их состояниям по умолчанию. Этот метод может быть явно вызван для выполнения очистки ресурсов.
Наконец, метод WebSocketHandle() возвращает базовый хэндл WebSocket HINTERNET.
//+------------------------------------------------------------------+ //| Closes a websocket client connection | //+------------------------------------------------------------------+ void CWebsocket::Close(void) { if(!initialized || hWebSocket == NULL) return; else client_disconnect(hWebSocket); } //+--------------------------------------------------------------------------+ //|method for abandoning a client connection. All previous server connection | //| parameters are reset to their default state | //+--------------------------------------------------------------------------+ void CWebsocket::Abort(void) { client_reset(hWebSocket); reset(); } //+------------------------------------------------------------------+ //| websocket handle | //+------------------------------------------------------------------+ HINTERNET CWebsocket::WebSocketHandle(void) { if(hWebSocket!=NULL) return client_websocket_handle(hWebSocket); else return NULL; }
Использование библиотеки DLL
В данном разделе представлена иллюстративная программа, разработанная для демонстрации использования asyncwinhttpwebsockets.dll. Программа включает в себя графический пользовательский интерфейс (GUI), устанавливающий подключение к сервису websocket echo, размещенному по адресу https://echo.websocket.org. Этот ресурс специально предназначен для тестирования клиентских реализаций WebSocket. Для создания этого приложения необходима свободно доступная, простая и быстрая библиотека MQL5 с графическим интерфейсом пользователя. Программа реализована в виде советника (EA) в среде MetaTrader 5. В пользовательском интерфейсе есть две кнопки, позволяющие устанавливать и завершать подключение к указанному серверу. Кроме того, предусмотрено поле для ввода текста, позволяющее пользователям вводить сообщения для передачи на сервер.
Выполняемые клиентом WebSocket операции записываются и отображаются, при этом для каждой записи в логе ставится временная метка для указания времени ее возникновения. Исходный код этой программы приведен ниже.
//+------------------------------------------------------------------+ //| Echo.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <EasyAndFastGUI\WndCreate.mqh> #include <asyncwebsocket.mqh> //+------------------------------------------------------------------+ //| Gui application class | //+------------------------------------------------------------------+ class CApp:public CWndCreate { protected: CWindow m_window; //main window CTextEdit m_rx; //text input to specify messages to be sent CTable m_tx; //text box displaying received messages from the server CButton m_connect; //connect button CButton m_disconnect; //disconnect button CTimeCounter m_timer_counter; //On timer objects CWebsocket* m_websocket; //websocket connection public: CApp(void); //constructor ~CApp(void); //destructor void OnInitEvent(void); void OnDeinitEvent(const int reason); virtual void OnEvent(const int id, const long &lparam, const double &dparam,const string &sparam); void OnTimerEvent(void); bool CreateGUI(void); protected: private: uint m_row_index; void EditTable(const string newtext); }; //+------------------------------------------------------------------+ //| constructor | //+------------------------------------------------------------------+ CApp::CApp(void) { m_row_index = 0; m_timer_counter.SetParameters(10,50); m_websocket = new CWebsocket(); } //+------------------------------------------------------------------+ //| destructor | //+------------------------------------------------------------------+ CApp::~CApp(void) { if(CheckPointer(m_websocket) == POINTER_DYNAMIC) delete m_websocket; } //+------------------------------------------------------------------+ //| On initialization | //+------------------------------------------------------------------+ void CApp::OnInitEvent(void) { } //+------------------------------------------------------------------+ //| On DeInitilization | //+------------------------------------------------------------------+ void CApp::OnDeinitEvent(const int reason) { CWndEvents::Destroy(); } //+------------------------------------------------------------------+ //| on timer event | //+------------------------------------------------------------------+ void CApp::OnTimerEvent(void) { CWndEvents::OnTimerEvent(); if(m_timer_counter.CheckTimeCounter()) { ENUM_WEBSOCKET_STATE client_state = m_websocket.ClientState(); ulong operation = m_websocket.CallBackResult(); switch((int)operation) { case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE: if(client_state == CLOSED) { EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnected]"); m_websocket.Abort(); } break; case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: if(client_state!=POLLING) { m_websocket.Poll(); EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Send Complete]"); } break; case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: if(m_websocket.ReadAvailable()) { string response; m_websocket.ReadString(response); EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Received]-> "+response); } break; case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Error]-> "+m_websocket.LastErrorMessage()); m_websocket.Abort(); break; default: break; } } } //+------------------------------------------------------------------+ //| create the gui | //+------------------------------------------------------------------+ bool CApp::CreateGUI(void) { //---check the websocket object if(CheckPointer(m_websocket) == POINTER_INVALID) { Print(__FUNCTION__," Failed to create websocket client object ", GetLastError()); return false; } //---initialize window creation if(!CWndCreate::CreateWindow(m_window,"Connect to https://echo.websocket.org Echo Server ",1,1,750,300,true,false,true,false)) return(false); //--- if(!CWndCreate::CreateTextEdit(m_rx,"",m_window,0,false,0,25,750,750,"Click connect button below, input your message here, then press enter key to send")) return(false); //--- if(!CWndCreate::CreateButton(m_connect,"Connect",m_window,0,5,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE)) return(false); //---create text edit for width in frequency units if(!CWndCreate::CreateButton(m_disconnect,"Disonnect",m_window,0,500,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE)) return(false); //---create text edit for amount of padding string tableheader[1] = {"Client Operations Log"}; if(!CWndCreate::CreateTable(m_tx,m_window,0,1,10,tableheader,5,75,0,0,true,true,5)) return(false); //--- m_tx.TextAlign(0,ALIGN_LEFT); m_tx.ShowTooltip(false); m_tx.DataType(0,TYPE_STRING); m_tx.IsDropdown(false); m_tx.SelectableRow(false); int cwidth[1] = {740}; m_tx.ColumnsWidth(cwidth); //---init events CWndEvents::CompletedGUI(); //--- return(true); } //+------------------------------------------------------------------+ //| edit the table | //+------------------------------------------------------------------+ void CApp::EditTable(const string newtext) { if(newtext==NULL) return; if((m_row_index+1)==m_tx.RowsTotal()) { m_tx.AddRow(m_row_index+1,true); m_tx.Update(); } m_tx.SetValue(0,m_row_index++,newtext,0,true); m_tx.Update(true); } //+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CApp::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { if(id==CHARTEVENT_CUSTOM+ON_END_EDIT) { if(lparam==m_rx.Id()) { if(m_websocket.ClientState() == CONNECTED) { string textinput = m_rx.GetValue(); if(StringLen(textinput)>0) { EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Sending]-> "+textinput); m_websocket.SendString(textinput); } } } return; } else if(id == CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { if(lparam==m_connect.Id()) { if(m_websocket.ClientState() != CONNECTED) { EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connecting]"); if(m_websocket.Connect("https://echo.websocket.org/")) { EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connected]"); m_websocket.Poll(); } else EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[FailedToConnect]"); } return; } if(lparam==m_disconnect.Id()) { if(m_websocket.ClientState() != CLOSED) { EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnecting]"); m_websocket.Close(); } } return; } } //+------------------------------------------------------------------+ CApp app; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(void) { ulong tick_counter=::GetTickCount(); //--- app.OnInitEvent(); //--- if(!app.CreateGUI()) { ::Print(__FUNCTION__," > error"); return(INIT_FAILED); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { app.OnDeinitEvent(reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(void) { } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer(void) { app.OnTimerEvent(); } //+------------------------------------------------------------------+ //| Trade function | //+------------------------------------------------------------------+ void OnTrade(void) { } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { app.ChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+
На рисунке ниже показано, как работает программа.

Заключение
В настоящей статье подробно описана разработка клиента WebSocket для MetaTrader 5 с использованием библиотеки WinHTTP в асинхронном режиме работы. Для инкапсуляции этой функциональности создан специальный класс и его реализация продемонстрирована в советнике (EA), предназначенном для взаимодействия с сервером echo, размещенным по адресу echo.websocket.org. Полный исходный код, включая код библиотеки динамических ссылок, представлен в дополнительных материалах. В частности, исходные файлы C++, а также файл конфигурации сборки CMakeLists.txt находятся в указанном каталоге C++. Кроме того, каталог MQL5 в дополнительных материалах содержит предварительно скомпилированный файл asyncwinhttpwebsockets.dll для немедленного развертывания.
Для пользователей, желающих создать клиентскую библиотеку на основе предоставленного исходного кода, требуется система сборки CMake. Если установлена система CMake, можно вызвать графический пользовательский интерфейс (cmake-gui). Затем пользователь должен указать каталог исходного кода, который соответствует расположению файла CMakeLists.txt (например, MqlWebsocketsClientDLL\Source\C++), и отдельный каталог сборки, который можно создать в любом желаемом месте.
После этого нажатие кнопки "Configure" (Настроить) запустит процесс настройки. В диалоговом окне пользователю будет предложено "Указать генератор для этого проекта", где следует выбрать соответствующую версию Visual Studio, установленную в системе. В параметре "Дополнительная платформа для генератора" пользователи могут указать "Win32" для компиляции 32-разрядной версии библиотеки DLL; в противном случае, если оставить это поле пустым, по умолчанию будет выполнена 64-разрядная компиляция. После нажатия кнопки "Finish" (Готово) CMake обработает первоначальную конфигурацию.
Затем появится уведомление об ошибке, указывающее на необходимость настройки определенных записей в файле CMakeLists.txt. Для решения этой проблемы пользователь должен найти запись с меткой "ADDITIONAL_LIBRARY_DEPENDENCIES", щелкнуть в соседнем поле и перейти в каталог, содержащий файл winhttp.lib.
После этого пользователь должен найти запись с меткой "OUTPUT_DIRECTORY_Xxx_RELEASE" (где "Xxx" обозначает архитектуру, X64 или X86) и указать соответствующий путь к папке "Библиотеки" ("Libraries") установки MetaTrader.
После настройки этих параметров повторное нажатие кнопки "Configure" должно завершить процесс настройки без дальнейших уведомлений об ошибках. Затем можно сгенерировать файл сборки, нажав кнопку "Сгенерировать» («Generate»). Успешная генерация активирует кнопку "Открыть проект" ("Open Project»), при нажатии на которую откроется сгенерированный файл проекта Visual Studio.
Чтобы создать библиотеку DLL, пользователь должен выбрать "Build", а затем "Build Solution" в Visual Studio. Результирующая библиотека DLL будет доступна в течение нескольких секунд.
| Название файла или папки | Описание |
|---|---|
| MqlWebsocketsClientDLL\Source\C++ | Папка содержит полные файлы исходного кода для asycnwinhttpwebsockets.dll |
| MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh | Содержит директиву import, в которой перечислены все функции, предоставляемые функцией asycnwinhttpwebsockets.dll |
| MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh | Содержит определение класса CWebsocket, который реализует функциональность, предоставляемую базовыми функциями библиотеки DLL. |
| MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 | Файл кода для советника, демонстрирующий применение библиотеки DLL |
| MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 | Скомпилированный советник, демонстрирующий применение библиотеки DLL |
| MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll | Скомпилированная библиотека DLL, обеспечивающая асинхронную функциональность WebSocket Winhttp |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17877
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От новичка до эксперта: Индикатор силы уровней поддержки и сопротивления (SRSI)
Трейдинг с экономическим календарем MQL5 (Часть 4): Обновление новостей в панели управления в реальном времени
Разработка инструментария для анализа движения цен (Часть 4): Советник Analytics Forecaster
Гауссовcкие процессы в машинном обучении (Часть 1): Модель классификации в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Это может помочь увидеть примеры кода:
Та же самая базовая предпосылка применима к советнику.Это отличная идея.
Я благодарю вас обоих за ответ на мой вопрос. Я, должно быть, пропустил определения перегруженных функций и прочитал только о первой. Возможно, вы знаете, достаточно ли Terminal умен для параллельной обработки вызовов iCustom, чтобы максимизировать использование процессора, поскольку я планирую варьировать параметр символа для каждой из 28 пар и планирую иметь несколько вызовов iCustom, как Brooky Trend Strength.
Также может ли кто-нибудь из вас сказать мне, где я могу разместить комментарии об ошибках в MQ5, а также где можно разместить предложения для администраторов Mq. Я нашел несколько, в последнее время разница в Bars между терминалом и тестером стратегий. Также, у меня есть 3 экрана с основным дисплеем в крайнем левом углу. Пытаясь переместить панель. Например, панели Navigator или Market, слева направо, очень утомительно. Указатель мыши находится на самом левом экране, но перетаскиваемая панель находится в середине. Я думаю, что либо терминал, либо Windows сходят с ума, когда мышь перемещается на один пиксель, а затем переключает дисплеи, чтобы переместить панель на один пиксель и обратно.
Bars() икает там, где отсутствуют данные о цене - там, где rates_total этого не делает. Если я правильно помню, что читал в прошлом, Bars() можно исправить, обратившись к временным меткам. Возможно, стоит поискать.
У меня есть 3 экрана с основным дисплеем в крайнем левом углу. Попытка переместить панель, например панель Navigator или Market, слева направо очень утомительна. Указатель мыши находится на самом левом экране, но перетаскиваемая панель находится в середине. Я думаю, что либо терминал, либо Windows сходят с ума, когда мышь перемещается на один пиксель, а затем переключает дисплеи, чтобы переместить панель на один пиксель и обратно.
Я действительно не знаю в этом вопросе. У меня есть 3 компьютера, каждый из которых имеет свой собственный монитор и терминал. Я знаю, что в Windows обычно есть настройки отображения нескольких мониторов, включая картинку в картинке, возможно, в качестве обходного пути.
Может ли кто-нибудь еще с реальными несколькими мониторами на одной машине ответить здесь, пожалуйста?
Отличная информация!!!
Спасибо, Райан, ваш комментарий по поводу bars vs rates_total подходит. Моя проблема в том, что в терминале они идентичны, но в STrategy Tester Visualize, Bars на один больше, что привело к тому, что я не дочитал документацию до конца. Я собираюсь взять ваш вклад и использовать его для iCustom. Я предполагаю, что должен быть отдельный адрес iCustom для каждой комбинации символов и временных характеристик.
Кроме того, есть ли способ для советника отображать текст на экране в тестере стратегий? В Mq4 он делал это автоматически, но не сейчас. Я использую много объектов класса для отображения информации, и размещение второй копии в шаблоне еще больше замедляет работу тестера стратегий.
Что касается трехпанельного дисплея, я думаю, проблема в том, что терминал не обновляет местоположение монитора, когда мышь перемещается с экрана 2 на экран 1.
У меня есть 2 мини-ПК, каждый из которых поддерживает 3 монитора, поэтому я подключил 3 экрана к обоим мини-ПК и использую HDMI1 для одного ПК и HDMI2 для другого. Отлично работает с 43-дюймовыми Fire Tv, хотя вы должны убедиться, что пульты правильно настроены для управления только одним монитором (позвоните в службу поддержки amazon). Единственный недостаток - кнопка выключения отключает все мониторы, и иногда мне нужно вытащить вилку, чтобы синхронизировать питание.
CapeCoddah
Кроме того, есть ли способ для советника отображать текст на экране в тестере стратегий? В Mq4 он делал это автоматически, но не сейчас. Я использую много объектов класса для отображения информации, и размещение второй копии в шаблоне еще больше замедляет работу тестера стратегий.
На 3-панельном дисплее, я думаю, проблема в том, что терминал не обновляет должным образом местоположение монитора, когда мышь перемещается с экрана 2 на экран 1.