WebSocket para MetaTrader 5: conexões assíncronas no lado do cliente usando a API do Windows
Introdução
No artigo "WebSocket para MetaTrader 5: uso da API do Windows", foi apresentado o uso da API do Windows para implementar um cliente WebSocket em aplicações MetaTrader 5. A implementação apresentada ali se limitava ao modo síncrono.
Neste artigo, voltaremos a examinar o uso da API do Windows para criar um cliente WebSocket para programas MetaTrader 5, com o objetivo de viabilizar funcionalidade assíncrona no lado do cliente. Na prática, isso exige a criação de uma DLL personalizada, que exporta funções adequadas para integração com aplicações MetaTrader 5.
Assim, este artigo aborda o desenvolvimento da DLL e, em seguida, será apresentada uma demonstração de seu uso em um programa MetaTrader 5.
Modo assíncrono do WinHTTP
Os pré-requisitos para o funcionamento assíncrono na biblioteca WinHTTP, conforme indicado em sua documentação, são dois. Em primeiro lugar, durante a chamada da função WinHttpOpen, o handle da sessão deve ser configurado com a flag WINHTTP_FLAG_ASYNC ou 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;
Após a criação de um handle de sessão válido, os usuários devem registrar uma função de callback para receber notificações sobre vários eventos associados a chamadas específicas de funções WinHTTP. Essa função de callback de status recebe notificações sobre o andamento das operações assíncronas por meio de flags de notificação.
O registro da função de callback é feito por meio da função WinHttpSetStatusCallback, que também permite especificar as flags de notificação que serão tratadas pelo callback. Os usuários podem optar por receber todas as notificações ou um subconjunto mais restrito. Além disso, callbacks separados podem ser atribuídos a handles de sessão, de solicitação e de WebSocket, respectivamente.
É importante observar que o registro da função de callback imediatamente após a criação do handle de sessão não é obrigatório; a função WinHttpSetStatusCallback pode ser chamada para qualquer handle HINTERNET válido em qualquer etapa anterior ou durante a inicialização da conexão WebSocket.
if (!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this))) { // Handle error ErrorCode = GetLastError(); return ErrorCode; } if (WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK) { ErrorCode = GetLastError(); return ErrorCode; }
A assinatura da função de callback definida pelo usuário inclui um parâmetro que aponta para uma estrutura de dados definida pelo usuário, normalmente chamada de valor de contexto. Esse mecanismo facilita a passagem de dados por meio da função de callback.
O valor de contexto deve ser especificado antes do registro da função de callback por meio de uma chamada à função WinHttpSetOption com a flag de opção WINHTTP_OPTION_CONTEXT_VALUE. Cabe observar que, nos testes empíricos, surgiram dificuldades para obter de forma confiável o valor de contexto registrado por esse método.
Embora não se possa descartar completamente a possibilidade de um erro de implementação, a falha recorrente exigiu o uso de uma variável global como alternativa, e esse detalhe será examinado mais adiante, na discussão posterior da biblioteca DLL.
Por fim, um ponto importante sobre a função de callback definida pelo usuário é a necessidade de thread safety. No entanto, como este trabalho envolve a criação de uma biblioteca DLL para uso no ambiente MetaTrader 5, essa restrição pode ser flexibilizada. Isso se deve à natureza essencialmente single-thread dos programas MetaTrader 5 e, embora o código da DLL seja executado no pool de threads do processo de carregamento, apenas uma thread fica ativa nos programas MetaTrader 5.
void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) Implementação da biblioteca DLL
A biblioteca DLL é criada no Visual Studio com o uso da linguagem de programação C++. Essa etapa exige a instalação da carga de trabalho "C++ Desktop Development" no Visual Studio, juntamente com o kit de desenvolvimento de software (SDK) do Windows 10 ou do Windows 11. O pacote SDK do Windows é um requisito indispensável, pois fornece o arquivo de biblioteca WinHTTP (.lib), ao qual a biblioteca DLL será vinculada durante a compilação. A biblioteca DLL resultante contém pelo menos três componentes principais.
O primeiro é a classe que encapsula as funcionalidades centrais do cliente WinHTTP WebSocket. O segundo é uma função de callback separada, que opera em conjunto com uma variável global e facilita a manipulação da conexão WebSocket tanto dentro do escopo da função de callback quanto fora dele. O terceiro componente consiste em um conjunto de funções de wrapper simplificadas, que serão disponibilizadas pela biblioteca DLL para uso em programas MetaTrader 5. A implementação começa com o código definido no arquivo de cabeçalho asyncwebsocketclient.h.
Esse arquivo de cabeçalho começa com a declaração da classe WebSocketClient, em que cada instância representa uma conexão de cliente separada.
// 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); };
Também é definida a estrutura Frame, usada em conjunto com a classe para representar um frame de mensagem.
struct Frame { std::vector<BYTE>frame_buffer; WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type; DWORD frame_size; };
Em seguida, para descrever os diferentes estados da conexão WebSocket, é declarada a enumeração ENUM_WEBSOCKET_STATE.
// client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
Na sequência, asyncwebsocketclient.h declara uma variável global chamada clients. Essa variável é um contêiner, mais especificamente, um map, destinado ao armazenamento de conexões WebSocket ativas. O escopo global desse contêiner map o torna acessível a qualquer função de callback definida na biblioteca.
// container for websocket objects accessible to callback function extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;
O arquivo asyncwebsocketclient.h é concluído com a definição de um conjunto de funções definidas pelo especificador WEBSOCK_API. Esse especificador serve para marcar essas funções para exportação pela DLL. Essas funções compõem as funções wrapper mencionadas anteriormente e constituem a interface por meio da qual os desenvolvedores interagirão com a biblioteca DLL em suas aplicações 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);
Ao analisar a definição da função WebSocketCallback(), observa-se o uso da variável global clients para gerenciar notificações. A implementação atual está configurada para tratar as chamadas notificações de término de operação descritas na documentação do WinHTTP. Essas notificações de término de operação são acionadas após a conclusão bem-sucedida de qualquer operação assíncrona. Por exemplo, WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE sinaliza a conclusão de uma operação de envio, enquanto WINHTTP_CALLBACK_STATUS_READ_COMPLETE indica a conclusão de uma operação de leitura.
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; } } }
O argumento hInternet da função de callback corresponde ao handle HINTERNET no qual o callback foi originalmente registrado. Esse handle é usado para acessar um elemento no contêiner global clients, do tipo map, retornando um ponteiro para uma instância de WebSocketClient. A notificação específica do callback é informada pelo argumento dwInternetStatus. Vale observar que os dados representados pelos argumentos lpvStatusInformation e dwStatusInformationLength variam conforme o valor de dwInternetStatus.
Por exemplo, quando dwInternetStatus assume os valores WINHTTP_CALLBACK_STATUS_READ_COMPLETE ou WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE, o parâmetro lpvStatusInformation contém um ponteiro para a estrutura WINHTTP_WEB_SOCKET_STATUS, e dwStatusInformationLength indica o tamanho dos dados associados.
Nossa implementação trata apenas parte das notificações fornecidas por essa função de callback, cada uma das quais leva a uma mudança de estado na instância correspondente de WebSocketClient. Em particular, o método OnCallBack() registra os códigos de estado associados a essas notificações. Essas informações são armazenadas na instância de WebSocketClient, onde podem ser acessadas pelos usuários por meio de uma função de wrapper.
VOID WebSocketClient::OnCallBack(const DWORD operation)
{
completed_websocket_operation = operation;
} O método OnReadComplete() da classe WebSocketClient é responsável por transferir os dados brutos para o buffer de frames na fila, a partir do qual os usuários podem posteriormente verificar se há dados disponíveis para extração.
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); }
O método OnSendComplete() atualiza os campos internos que indicam uma operação de envio bem-sucedida, o que também altera o estado do cliente WebSocket.
VOID WebSocketClient::OnSendComplete(const DWORD sent) { bytesTX = sent; status = ENUM_WEBSOCKET_STATE::CONNECTED; return; }
Por fim, o método onError() registra qualquer informação de erro fornecida por meio do argumento lpvStatusInformation.
VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result) { SetError(result->dwError); Reset(false); }
A conexão com o servidor é estabelecida por meio da função client_connect(). Essa função é chamada com o endereço do servidor, na forma de string, o número da porta, na forma de inteiro, um valor lógico que indica se a conexão deve ser segura, e um ponteiro para HINTERNET. A função retorna um valor DWORD e também define o argumento HINTERNET. Se ocorrer algum erro durante o estabelecimento da conexão, a função definirá o argumento HINTERNET como NULL e retornará um código de erro diferente de zero.
A função interna client_connect() inicializa uma instância da classe WebSocketClient e a utiliza para estabelecer a conexão. Depois que a conexão é estabelecida com sucesso, um ponteiro para essa instância de WebSocketClient é armazenado no contêiner global clients, e o handle WebSocket da instância é usado como chave única. Esse handle WebSocket é usado para identificar unicamente a conexão WebSocket, tanto nas operações internas da biblioteca DLL quanto nas operações externas executadas pelo programa MetaTrader 5 que faz a chamada.
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; }
O método Connect() da classe WebSocketClient é responsável por inicializar e estabelecer conexões WebSocket. Inicialmente, a conexão ocorre de forma síncrona. Depois disso, a função de callback é registrada no handle WebSocket por meio da chamada ao método EnableCallBack(), o que permite receber notificações sobre eventos assíncronos.
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; }
Depois de estabelecer a conexão com o servidor e obter um handle válido, os usuários podem iniciar a interação com a extremidade remota. O envio de dados ao servidor é feito por meio da chamada da função client_send(). Essa função exige os seguintes parâmetros: um handle WebSocket válido, um valor da enumeração WINHTTP_WEB_SOCKET_BUFFER_TYPE que indica o tipo do frame WebSocket transmitido, um array de BYTE que contém a carga útil e um argumento ulong que indica o tamanho do array de dados. A função retorna zero se nenhum erro imediato for detectado. Caso contrário, retorna um código de erro específico.
A função interna WinHttpWebSocketSend() é chamada de forma assíncrona. Portanto, o valor retornado por client_send() representa um status intermediário, que indica apenas a ausência de erros preliminares durante a preparação da operação de envio. O resultado da transmissão efetiva dos dados não é retornado de forma síncrona. Em vez disso, ele é informado de forma assíncrona por meio de uma notificação recebida pela função de callback registrada. Quando a operação de envio é bem-sucedida, a notificação esperada é WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE. Por outro lado, se ocorrer um erro durante qualquer operação, seja de envio ou de recebimento, a função de callback normalmente receberá a notificação de erro 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;
} As notificações recebidas pela função interna de callback podem ser consultadas com a função client_lastcallback_notification(). Essa função retorna a notificação mais recente recebida pelo callback para uma determinada conexão, identificada pelo handle WebSocket fornecido como único argumento. O trecho de código a seguir ilustra uma possível abordagem para tratar essas notificações em um programa MetaTrader 5. As constantes simbólicas correspondentes a essas notificações estão definidas no arquivo asyncwinhttp.mqh, derivado do arquivo de cabeçalho 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;
} Para receber os dados transmitidos pelo servidor, primeiro é necessário colocar o cliente no estado de polling por meio da chamada da função client_poll(). Essa ação aciona a função interna WinHttpWebSocketReceive() na classe WebSocketClient. Como ocorre na operação de envio, a função WinHTTP é chamada de forma assíncrona, o que resulta no retorno imediato de um status intermediário.
A classe WebSocketClient inclui buffers internos para armazenar os dados brutos à medida que chegam. Assim que a operação de leitura é concluída com sucesso, esses dados são colocados em uma fila na estrutura de dados interna. Esse procedimento é controlado pelo método OnReadComplete() da classe WebSocketClient. Quando a operação de leitura é concluída, o estado da conexão WebSocket muda, e ela deixa de escutar ativamente mensagens de entrada.
Isso significa que a solicitação assíncrona de leitura não é contínua e não corresponde a um polling contínuo. Para receber mensagens subsequentes do servidor, é necessário chamar novamente a função client_poll(). Em essência, client_poll() coloca o cliente WebSocket em um estado temporário de polling não bloqueante, captura os dados quando eles ficam disponíveis e, em seguida, aciona a notificação WINHTTP_CALLBACK_STATUS_READ_COMPLETE.
O estado atual do cliente WebSocket pode ser consultado com a função client_status(), que retorna um valor do tipo enumeração 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;
} A função client_read() facilita a recuperação dos dados brutos recebidos do servidor, que recebe os seguintes argumentos:
- um handle HINTERNET válido do WebSocket,
- uma referência a um array de BYTE previamente alocado, um valor ulong que define o tamanho desse array,
- uma referência a um valor WINHTTP_WEB_SOCKET_BUFFER_TYPE.
DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype) { DWORD rout = 0; if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end()) rout = WEBSOCKET_ERROR_INVALID_HANDLE; else clients[websocket_handle]->Read(out, out_size, buffertype); return rout; } DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle) { DWORD out = 0; if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end()) out = 0; else out = clients[websocket_handle]->ReadAvailable(); return out; }
Os códigos de erro podem ser obtidos com a função client_lasterror(). A função retorna um valor DWORD correspondente ao último erro detectado. Os usuários também podem obter o valor atual do handle WebSocket por meio de client_websocket_handle(). Isso pode ser útil para verificar se o handle foi fechado.
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;
} O encerramento gradual da conexão com o servidor é iniciado com a chamada da função client_disconnect(). Essa função não retorna valor. No entanto, ela imediatamente coloca o cliente WebSocket em estado de fechamento. Se, posteriormente, um frame de fechamento correspondente for recebido do servidor, a notificação WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE será disparada, o que muda o estado do WebSocket para fechado.
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; }
A última função exportada pela biblioteca DLL é client_reset(). Idealmente, essa função deve ser chamada após se desconectar do servidor. Sua finalidade é liberar os buffers internos de memória associados à conexão encerrada. Embora o uso dessa função não seja estritamente obrigatório, ela pode ser útil para liberar recursos de memória que poderão ser necessários em outras partes da execução do programa. A chamada da função client_reset() efetivamente invalida todos os dados associados ao handle WebSocket especificado, incluindo códigos de erro, mensagens de erro e quaisquer dados ainda não lidos que permaneçam na fila interna de frames.
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
{
if(clients.find(websocket_handle) != clients.end())
{
clients[websocket_handle]->Free();
clients.erase(websocket_handle);
}
} Redefinindo a classe CWebsocket
Antes de passar à análise de uma aplicação MetaTrader 5 que utiliza as funções descritas na seção anterior, vamos redefinir a classe CWebsocket, discutida anteriormente no artigo já mencionado. Essa redefinição permitirá adaptar a classe para uso com o cliente WebSocket assíncrono recém-desenvolvido. O código-fonte dessa adaptação está no arquivo asyncwebsocket.mqh. p.
A enumeração ENUM_WEBSOCKET_STATE foi expandida para incluir estados adicionais, que refletem a natureza assíncrona do cliente. O estado de polling (POLLING) é acionado quando uma operação de leitura assíncrona é iniciada. Depois que o socket subjacente recebe os dados e os torna disponíveis para leitura, a função de callback sinaliza a conclusão da operação de leitura assíncrona, e o estado do cliente WebSocket retorna ao estado padrão: CONNECTED. Da mesma forma, uma operação de envio assíncrona faz o estado passar para ENVIANDO (SENDING). O resultado dessa operação é transmitido de forma assíncrona por meio da função de callback, de modo que, quando a transmissão é bem-sucedida, o cliente retorna ao estado padrão CONECTADO.
//+------------------------------------------------------------------+ //| client state enumeration | //+------------------------------------------------------------------+ // client state enum ENUM_WEBSOCKET_STATE { CLOSED = 0, CLOSING = 1, CONNECTING = 2, CONNECTED = 3, SENDING = 4, POLLING = 5 };
Vários métodos novos foram integrados à classe CWebsocket para viabilizar suas funcionalidades ampliadas. Os demais métodos mantêm suas assinaturas originais, e apenas suas implementações internas foram alteradas para incorporar a nova dependência da DLL. São eles:
Connect(): Este método é o ponto de entrada para estabelecer a conexão com o servidor. Ele recebe os seguintes parâmetros:- _serveraddress: O endereço completo do servidor (tipo de dados string).
- _port: O número da porta do servidor (tipo de dados ushort).
- _secure: Um valor lógico que indica se deve ser estabelecida uma conexão segura (tipo lógico).
A implementação desse método foi significativamente simplificada, já que a maior parte da lógica de conexão agora é tratada pela DLL subjacente.
//+------------------------------------------------------------------------------------------------------+ //|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); }
Se o método Connect() retornar o valor lógico true, indicando que a conexão foi estabelecida com sucesso, será possível iniciar a transmissão de dados pelo cliente WebSocket. Para esse fim, há dois métodos:
- SendString(): Este método recebe uma string como entrada.
- Send(): Este método recebe um array de unsigned char como único parâmetro.
Ambos os métodos retornam true quando a operação de envio é iniciada com sucesso e, internamente, chamam o método privado clientsend(), que controla todas as operações de envio da classe.
//+------------------------------------------------------------------+ //|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)); }
A chamada do método Poll() inicia uma operação de leitura assíncrona em nível mais baixo, colocando o cliente WebSocket no estado de POLLING. Esse estado indica que o cliente está aguardando uma resposta do servidor.
//+------------------------------------------------------------------+ //| asynchronous read operation (polls for server response) | //+------------------------------------------------------------------+ ulong CWebsocket::Poll(void) { if(hWebSocket!=NULL) return client_poll(hWebSocket); else return WEBSOCKET_ERROR_INVALID_HANDLE; }
Para verificar se os dados foram recebidos e lidos com sucesso pelo cliente, o usuário dispõe de duas opções:
- CallBackResult(): Este método verifica a última notificação recebida da função de callback. O resultado esperado de uma operação de leitura bem-sucedida é uma notificação de conclusão de leitura.
- ReadAvailable(): Este método retorna o tamanho, em bytes, dos dados atualmente disponíveis para extração do buffer interno.
//+------------------------------------------------------------------+ //| call back notification | //+------------------------------------------------------------------+ ulong CWebsocket::CallBackResult(void) { if(hWebSocket!=NULL) return client_lastcallback_notification(hWebSocket); else return WINHTTP_CALLBACK_STATUS_DEFAULT; } //+------------------------------------------------------------------+ //| check if any data has read from the server | //+------------------------------------------------------------------+ ulong CWebsocket::ReadAvailable(void) { if(hWebSocket!=NULL) return(client_readable(hWebSocket)); else return 0; }
Em seguida, os dados brutos transmitidos pelo servidor podem ser acessados por meio dos métodos Read() ou ReadString(). Ambos os métodos retornam o tamanho dos dados recebidos. A função ReadString() exige uma variável do tipo string passada por referência, na qual os dados recebidos serão gravados, enquanto a função Read() grava os dados em um array de unsigned char.
//+------------------------------------------------------------------+ //|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); }
Quando o cliente WebSocket não for mais necessário, a conexão com o servidor pode ser encerrada com o método Close(). O método Abort() difere do método Close() porque fecha a conexão WebSocket à força, encerrando os handles subjacentes, o que também restaura as propriedades internas da classe aos seus valores padrão. Esse método pode ser chamado explicitamente para liberar recursos.
Por fim, o método WebSocketHandle() retorna o handle HINTERNET subjacente do WebSocket.
//+------------------------------------------------------------------+ //| Closes a websocket client connection | //+------------------------------------------------------------------+ void CWebsocket::Close(void) { if(!initialized || hWebSocket == NULL) return; else client_disconnect(hWebSocket); } //+--------------------------------------------------------------------------+ //|method for abandoning a client connection. All previous server connection | //| parameters are reset to their default state | //+--------------------------------------------------------------------------+ void CWebsocket::Abort(void) { client_reset(hWebSocket); reset(); } //+------------------------------------------------------------------+ //| websocket handle | //+------------------------------------------------------------------+ HINTERNET CWebsocket::WebSocketHandle(void) { if(hWebSocket!=NULL) return client_websocket_handle(hWebSocket); else return NULL; }
Uso da biblioteca DLL
Esta seção apresenta um programa ilustrativo para demonstrar o uso de asyncwinhttpwebsockets.dll. O programa inclui uma interface gráfica do usuário (GUI) que estabelece conexão com o serviço de echo WebSocket hospedado em https://echo.websocket.org. Esse recurso é destinado especificamente ao teste de implementações de cliente WebSocket. Para criar essa aplicação, é necessária uma biblioteca MQL5 gratuita, simples e rápida para interface gráfica do usuário. O programa foi implementado como um EA no ambiente MetaTrader 5. A interface do usuário inclui dois botões que permitem estabelecer e encerrar a conexão com o servidor especificado. Além disso, há um campo de entrada de texto que permite aos usuários digitar mensagens para envio ao servidor.
As operações executadas pelo cliente WebSocket são registradas no log e exibidas, com cada entrada do log recebendo um carimbo de data e hora para indicar quando ocorreu. O código-fonte desse programa é apresentado a seguir.
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
A figura abaixo mostra o funcionamento do programa.

Conclusão
Neste artigo, descreveu-se em detalhe o desenvolvimento de um cliente WebSocket para MetaTrader 5 com o uso da biblioteca WinHTTP em modo assíncrono. Para encapsular essa funcionalidade, foi criada uma classe específica, e sua implementação foi demonstrada em um EA destinado à interação com o servidor de eco hospedado em echo.websocket.org. O código-fonte completo, incluindo o código da biblioteca DLL, está disponível nos materiais complementares. Em particular, os arquivos-fonte em C++, bem como o arquivo de configuração de build CMakeLists.txt, estão localizados no diretório C++ indicado. Além disso, o diretório MQL5 nos materiais complementares contém o arquivo asyncwinhttpwebsockets.dll pré-compilado, pronto para uso imediato.
Os usuários que desejarem criar uma biblioteca cliente com base no código-fonte fornecido precisarão do sistema de build CMake. Se o CMake estiver instalado, é possível abrir a interface gráfica cmake-gui. Em seguida, o usuário deve indicar o diretório do código-fonte onde está localizado o arquivo CMakeLists.txt, por exemplo, MqlWebsocketsClientDLL\Source\C++, e um diretório de build separado, que pode ser criado em qualquer local.
Depois disso, clique no botão "Configure" para iniciar o processo de configuração. Na caixa de diálogo, será solicitado ao usuário que especifique o gerador deste projeto, e ali o usuário deverá selecionar a versão do Visual Studio instalada no sistema. No campo "Optional platform for generator", os usuários podem indicar "Win32" para compilar a versão de 32 bits da biblioteca DLL. Caso contrário, se esse campo for deixado em branco, a compilação de 64 bits será feita por padrão. Depois de clicar em "Finish", o CMake executará a configuração inicial.
Em seguida, aparecerá uma mensagem de erro informando que determinadas entradas do arquivo CMakeLists.txt precisam ser configuradas. Para resolver esse problema, o usuário deve localizar o item "ADDITIONAL_LIBRARY_DEPENDENCIES", clicar no campo ao lado e navegar até o diretório que contém o arquivo winhttp.lib.
Depois disso, o usuário deve localizar a entrada chamada "OUTPUT_DIRECTORY_Xxx_RELEASE", em que "Xxx" representa a arquitetura, X64 ou X86, e informar o caminho da pasta "Libraries" da instalação do MetaTrader.
Após configurar esses parâmetros, clicar novamente no botão "Configure" deverá concluir a configuração sem novas mensagens de erro. Em seguida, será possível gerar os arquivos de build clicando no botão "Generate". Quando a geração for concluída com sucesso, o botão "Open Project" será ativado. Ao clicar nele, o projeto gerado do Visual Studio será aberto.
Para criar a biblioteca DLL, o usuário deve selecionar "Build" e, em seguida, "Build Solution" no Visual Studio. A biblioteca DLL será gerada em poucos segundos.
| Nome do arquivo ou da pasta | Descrição |
|---|---|
| MqlWebsocketsClientDLL\Source\C++ | A pasta contém os arquivos completos do código-fonte de asycnwinhttpwebsockets.dll |
| MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh | Inclui a diretiva import, que lista todas as funções fornecidas por asycnwinhttpwebsockets.dll |
| MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh | Contém a definição da classe CWebsocket, que implementa a funcionalidade fornecida pelas funções básicas da biblioteca DLL. |
| MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 | Arquivo-fonte do EA que demonstra o uso da biblioteca DLL |
| MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 | EA compilado que demonstra o uso da biblioteca DLL |
| MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll | Biblioteca DLL compilada que fornece funcionalidade WebSocket assíncrona com WinHTTP |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17877
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Criação de interfaces gráficas dinâmicas em MQL5 por meio de interpolação bicúbica
Integração de um modelo de IA a uma estratégia de trading existente em MQL5
Está chegando o novo MetaTrader 5 e MQL5
De Iniciante a Especialista: Indicador de Força de Suporte e Resistência (SRSI)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Pode ser útil ver alguns exemplos de código em:
A mesma premissa básica se aplica a um EA.Essa é uma excelente ideia.
Agradeço a ambos pela resposta à minha pergunta. Devo ter perdido as definições de funções sobrecarregadas e só li sobre a primeira. Vocês sabem se o Terminal é inteligente o suficiente para processar em paralelo as chamadas iCustom para maximizar a utilização do processador, já que pretendo variar o parâmetro do símbolo para cada um dos 28 pares e pretendo ter várias chamadas iCustom, como a Brooky Trend Strength.
Algum de vocês pode me dizer onde posso postar comentários sobre bugs no MQ5 e também onde posso fazer sugestões para os administradores do Mq. Encontrei alguns, mais recentemente a diferença de barras entre o terminal e o testador de estratégia. Além disso, tenho uma configuração de 3 telas com a tela principal na extrema esquerda. Estou tentando mover um painel, como o Navigator ou o Market. O ponteiro de arrastar do mouse está no lado esquerdo da tela, mas o painel de arrastar está no meio. Acho que o Terminal ou o Windows está enlouquecendo quando o mouse move um pixel e, em seguida, alterna as telas para mover o painel um pixel e vice-versa
A função Bars() apresenta problemas quando há dados de preço ausentes, enquanto a função rates_total não apresenta. Se me lembro bem do que li no passado, o Bars() pode ser corrigido fazendo referência a registros de data e hora. Talvez valha a pena fazer uma pesquisa.
Tenho uma configuração de 3 telas com a tela principal na extrema esquerda. Tentar mover um painel, como os painéis Navigator ou Market, da esquerda para a direita é muito tedioso. O ponteiro do mouse de arrastar está na tela mais à esquerda, mas o painel de arrastar está no meio. Acho que o Terminal ou o Windows está enlouquecendo quando o mouse move um pixel e, em seguida, alterna as telas para mover o painel um pixel e vice-versa
Eu realmente não sei o que fazer com isso. Tenho três computadores, cada um com seu próprio monitor e terminal. Sei que o Windows geralmente tem configurações de exibição de vários monitores, incluindo picture-in-picture, talvez como uma solução alternativa.
Alguém que tenha vários monitores reais em uma única máquina pode entrar em contato comigo, por favor?
Ótimas informações!!!
Meu problema é que os dois são idênticos no Terminal, mas no STrategy Tester Visualize, Bars é um maior, o que me levou a fazer bobagem por não ter lido a documentação até o fim. Vou pegar sua opinião e usá-la no iCustom. Presumo que deve haver um endereço iCustom separado para cada combinação de especificações de símbolo e tempo.
Além disso, existe alguma maneira de um EA exibir texto na tela do Strategy Tester? No Mq4, ele fazia isso automaticamente, mas agora não. Eu uso muitos objetos de classe para exibir informações e colocar uma segunda cópia no modelo torna o Strategy Tester ainda mais lento.
Na tela de 3 painéis, acho que o problema é que o terminal não atualiza corretamente o local do monitor quando o mouse se move da tela 2 para a tela 1.
Tenho dois minicomputadores que suportam três monitores cada um, portanto, tenho as três telas conectadas aos dois minicomputadores e uso HDMI1 para um computador e HDMI2 para o outro. Funciona muito bem com TVs Fire de 43 polegadas, embora seja necessário verificar se os controles remotos estão configurados corretamente para controlar apenas um monitor (ligue para o suporte da Amazon). A única desvantagem é que o botão liga/desliga desliga todos os monitores e, às vezes, preciso puxar o plugue para sincronizar a energia.
CapeCoddah
Além disso, existe alguma maneira de um EA exibir o texto na tela no Strategy Tester? No Mq4, ele fazia isso automaticamente, mas agora não. Eu uso muitos objetos de classe para exibir informações e colocar uma segunda cópia no modelo torna o Strategy Tester ainda mais lento.
No monitor de 3 painéis, acho que o problema é que o terminal não atualiza corretamente o local do monitor quando o mouse se move da tela 2 para a tela 1.