English Deutsch 日本語
preview
WebSocket для MetaTrader 5 — Асинхронные клиентские соединения с помощью Windows API

WebSocket для MetaTrader 5 — Асинхронные клиентские соединения с помощью Windows API

MetaTrader 5Примеры |
448 9
Francis Dube
Francis Dube

Введение

В статье «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.
Полученные данные записываются в предоставленный массив BYTE, а тип кадра WebSocket копируется в аргумент buffertype. Важно отметить, что эта функция работает путем считывания данных из внутренней очереди полученных кадров, а не напрямую взаимодействует с сетевым сокетом. Следовательно, client_read() является синхронной операцией, независимой от асинхронных механизмов библиотеки WinHTTP. Ненулевое возвращаемое значение указывает на сбой при копировании данных из внутренней очереди. После успешного извлечения кадра с помощью этой функции кадр удаляется (деактивируется) из внутренней очереди. Функция client_readable() может быть вызвана для определения размера фрейма данных, который в данный момент находится в начале очереди фреймов, полученных от сервера.
DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype)
  {
   DWORD rout = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      rout = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      clients[websocket_handle]->Read(out, out_size, buffertype);

   return rout;
  }

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

   return out;
  }

Коды ошибок можно получить, вызвав функцию client_lasterror(). Функция возвращает значение DWORD для последней обнаруженной ошибки. Пользователи также могут получить текущее значение хэндла 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);
  }
//+------------------------------------------------------------------+

На рисунке ниже показано, как работает программа.

Echo EA Demonstration


Заключение

В настоящей статье подробно описана разработка клиента 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

Прикрепленные файлы |
asyncwinhttp.mqh (13.42 KB)
asyncwebsocket.mqh (19.09 KB)
Echo.mq5 (10.84 KB)
Echo.ex5 (306.48 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (9)
Shephard Mukachi
Shephard Mukachi | 2 мая 2025 в 21:17
Ryan L Johnson #:

Это может помочь увидеть примеры кода:

Та же самая базовая предпосылка применима к советнику.

Это отличная идея.

CapeCoddah
CapeCoddah | 15 мая 2025 в 23:47

Я благодарю вас обоих за ответ на мой вопрос. Я, должно быть, пропустил определения перегруженных функций и прочитал только о первой. Возможно, вы знаете, достаточно ли Terminal умен для параллельной обработки вызовов iCustom, чтобы максимизировать использование процессора, поскольку я планирую варьировать параметр символа для каждой из 28 пар и планирую иметь несколько вызовов iCustom, как Brooky Trend Strength.

Также может ли кто-нибудь из вас сказать мне, где я могу разместить комментарии об ошибках в MQ5, а также где можно разместить предложения для администраторов Mq. Я нашел несколько, в последнее время разница в Bars между терминалом и тестером стратегий. Также, у меня есть 3 экрана с основным дисплеем в крайнем левом углу. Пытаясь переместить панель. Например, панели Navigator или Market, слева направо, очень утомительно. Указатель мыши находится на самом левом экране, но перетаскиваемая панель находится в середине. Я думаю, что либо терминал, либо Windows сходят с ума, когда мышь перемещается на один пиксель, а затем переключает дисплеи, чтобы переместить панель на один пиксель и обратно.

Silk Road Trading LLC
Ryan L Johnson | 16 мая 2025 в 00:13
CapeCoddah тестером стратегий.

Bars() икает там, где отсутствуют данные о цене - там, где rates_total этого не делает. Если я правильно помню, что читал в прошлом, Bars() можно исправить, обратившись к временным меткам. Возможно, стоит поискать.

CapeCoddah #:
У меня есть 3 экрана с основным дисплеем в крайнем левом углу. Попытка переместить панель, например панель Navigator или Market, слева направо очень утомительна. Указатель мыши находится на самом левом экране, но перетаскиваемая панель находится в середине. Я думаю, что либо терминал, либо Windows сходят с ума, когда мышь перемещается на один пиксель, а затем переключает дисплеи, чтобы переместить панель на один пиксель и обратно.

Я действительно не знаю в этом вопросе. У меня есть 3 компьютера, каждый из которых имеет свой собственный монитор и терминал. Я знаю, что в Windows обычно есть настройки отображения нескольких мониторов, включая картинку в картинке, возможно, в качестве обходного пути.

Может ли кто-нибудь еще с реальными несколькими мониторами на одной машине ответить здесь, пожалуйста?

CapeCoddah
CapeCoddah | 16 мая 2025 в 09:42

Отличная информация!!!

Спасибо, Райан, ваш комментарий по поводу bars vs rates_total подходит. Моя проблема в том, что в терминале они идентичны, но в STrategy Tester Visualize, Bars на один больше, что привело к тому, что я не дочитал документацию до конца. Я собираюсь взять ваш вклад и использовать его для iCustom. Я предполагаю, что должен быть отдельный адрес iCustom для каждой комбинации символов и временных характеристик.

Кроме того, есть ли способ для советника отображать текст на экране в тестере стратегий? В Mq4 он делал это автоматически, но не сейчас. Я использую много объектов класса для отображения информации, и размещение второй копии в шаблоне еще больше замедляет работу тестера стратегий.

Что касается трехпанельного дисплея, я думаю, проблема в том, что терминал не обновляет местоположение монитора, когда мышь перемещается с экрана 2 на экран 1.

У меня есть 2 мини-ПК, каждый из которых поддерживает 3 монитора, поэтому я подключил 3 экрана к обоим мини-ПК и использую HDMI1 для одного ПК и HDMI2 для другого. Отлично работает с 43-дюймовыми Fire Tv, хотя вы должны убедиться, что пульты правильно настроены для управления только одним монитором (позвоните в службу поддержки amazon). Единственный недостаток - кнопка выключения отключает все мониторы, и иногда мне нужно вытащить вилку, чтобы синхронизировать питание.


CapeCoddah

Silk Road Trading LLC
Ryan L Johnson | 16 мая 2025 в 19:58
CapeCoddah STrategy Tester Visualize Bars на один больше, что привело к тому, что я не дочитал документацию до конца. Я собираюсь воспользоваться вашими рекомендациями и использовать их для iCustom. Я предполагаю, что для каждой комбинации спецификаций символа и времени должен быть отдельный адрес iCustom.

  1. Один файл индикатора в одном каталоге может быть повторно использован несколькими экземплярами iCustom().
  2. Один хэндл индикатора может быть повторно использован несколькими экземплярами CopyBuffer().
  3. Теперь я понимаю, почему вы используете Bars(), поскольку rates_total ограничивается одним таймфреймом. Предположительно, вы используете Bars() в отдельном цикле для каждого таймфрейма.

CapeCoddah #:
Кроме того, есть ли способ для советника отображать текст на экране в тестере стратегий? В Mq4 он делал это автоматически, но не сейчас. Я использую много объектов класса для отображения информации, и размещение второй копии в шаблоне еще больше замедляет работу тестера стратегий.
Насколько я знаю, нет. Вы уже используете единственный метод, который я знаю из справочной страницы Testing Visualization MT5.
CapeCoddah #:
На 3-панельном дисплее, я думаю, проблема в том, что терминал не обновляет должным образом местоположение монитора, когда мышь перемещается с экрана 2 на экран 1.
К сожалению, у меня нет возможности проверить это на своей собственной установке. Вы растягиваете один экран терминала MT5 на все мониторы? Я видел, как другие решали проблемы таким образом.
От новичка до эксперта: Индикатор силы уровней поддержки и сопротивления (SRSI) От новичка до эксперта: Индикатор силы уровней поддержки и сопротивления (SRSI)
В настоящей статье мы поделимся информацией о том, как использовать программирование на MQL5 для точного определения уровней рынка, различая более слабые и самые сильные уровни цен. Мы в полном объеме разработаем действующий Индикатор силы уровней поддержки и сопротивления (SRSI).
Трейдинг с экономическим календарем MQL5 (Часть 4): Обновление новостей в панели управления в реальном времени Трейдинг с экономическим календарем MQL5 (Часть 4): Обновление новостей в панели управления в реальном времени
В этой статье мы расширим возможности нашей панели экономического календаря, внедрив обновления новостей в реальном времени для поддержания актуальности рыночной информации. Мы интегрируем методы извлечения данных в реальном времени в MQL5 для непрерывного обновления событий на панели управления и повышения отзывчивости интерфейса. Это обновление обеспечивает нам доступ к последним экономическим новостям непосредственно с панели управления, оптимизируя торговые решения на основе самых свежих данных.
Разработка инструментария для анализа движения цен (Часть 4): Советник Analytics Forecaster Разработка инструментария для анализа движения цен (Часть 4): Советник Analytics Forecaster
Мы выходим за рамки простого просмотра проанализированных показателей на графиках и переходим к более широкой перспективе, которая включает интеграцию с Telegram. Это позволит отправлять важные результаты непосредственно на мобильное устройство через Telegram.
Гауссовcкие процессы в машинном обучении (Часть 1): Модель классификации в MQL5 Гауссовcкие процессы в машинном обучении (Часть 1): Модель классификации в MQL5
В данной статье мы рассмотрим модель классификации гауссовских процессов. Мы начнём с изучения её теоретических принципов, а затем перейдём к практической разработке библиотеки ГП на MQL5.