English Русский 中文 Español Deutsch 日本語
preview
WebSocket para MetaTrader 5: conexões assíncronas no lado do cliente usando a API do Windows

WebSocket para MetaTrader 5: conexões assíncronas no lado do cliente usando a API do Windows

MetaTrader 5Exemplos |
31 9
Francis Dube
Francis Dube

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.
Os dados recebidos são gravados no array de BYTE fornecido, e o tipo do frame WebSocket é copiado para o argumento buffertype. É importante observar que essa função lê os dados da fila interna de frames recebidos, sem interagir diretamente com o socket de rede. Portanto, client_read() é uma operação síncrona, independente dos mecanismos assíncronos da biblioteca WinHTTP. Um valor de retorno diferente de zero indica falha ao copiar os dados da fila interna. Depois que um frame é recuperado com sucesso por essa função, ele é removido da fila interna. A função client_readable() pode ser chamada para determinar o tamanho do frame de dados que, naquele momento, está no início da fila de frames recebidos do servidor.
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.

Demonstração do EA Echo


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

Arquivos anexados |
asyncwinhttp.mqh (13.42 KB)
asyncwebsocket.mqh (19.09 KB)
Echo.mq5 (10.84 KB)
Echo.ex5 (306.48 KB)
Últimos Comentários | Ir para discussão (9)
Shephard Mukachi
Shephard Mukachi | 2 mai. 2025 em 21:17
Ryan L Johnson #:

Pode ser útil ver alguns exemplos de código em:

A mesma premissa básica se aplica a um EA.

Essa é uma excelente ideia.

CapeCoddah
CapeCoddah | 15 mai. 2025 em 23:47

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

Ryan L Johnson
Ryan L Johnson | 16 mai. 2025 em 00:13
CapeCoddah testador de estratégia.

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.

CapeCoddah #:
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?

CapeCoddah
CapeCoddah | 16 mai. 2025 em 09:42

Ó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

Ryan L Johnson
Ryan L Johnson | 16 mai. 2025 em 19:58
CapeCoddah STrategy Tester Visualize, as Barras são um pouco maiores, o que me levou a cometer um erro 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.

  1. Um único arquivo de indicador em um único diretório pode ser reutilizado por várias instâncias de iCustom().
  2. Um único identificador de indicador pode ser reutilizado por várias instâncias de CopyBuffer().
  3. Agora entendo por que você está usando Bars(), já que rates_total sozinho é limitado a um único período de tempo. Presumivelmente, você está usando Bars() em um loop separado para cada período de tempo.

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.
Não que eu saiba. Você já está usando o único método que eu conheço da página de ajuda do MT5 Testing Visualization.
CapeCoddah #:
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.
Infelizmente, não tenho como testar isso com minha própria configuração. Você está esticando uma única tela do terminal MT5 em todos os monitores? Já vi outras pessoas resolverem problemas dessa forma.
Criação de interfaces gráficas dinâmicas em MQL5 por meio de interpolação bicúbica Criação de interfaces gráficas dinâmicas em MQL5 por meio de interpolação bicúbica
Neste artigo, vamos explorar interfaces gráficas dinâmicas em MQL5 que usam interpolação bicúbica para o redimensionamento de imagens com alta qualidade em gráficos de trading. Descreveremos em detalhes opções flexíveis de posicionamento, que permitem centralização dinâmica ou ancoragem aos cantos com deslocamentos ajustáveis.
Integração de um modelo de IA a uma estratégia de trading existente em MQL5 Integração de um modelo de IA a uma estratégia de trading existente em MQL5
Este artigo trata da integração de um modelo de IA treinado, por exemplo, um modelo LSTM para aprendizado por reforço ou um modelo preditivo baseado em machine learning, a uma estratégia de trading existente em MQL5.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
De Iniciante a Especialista: Indicador de Força de Suporte e Resistência (SRSI) De Iniciante a Especialista: Indicador de Força de Suporte e Resistência (SRSI)
Neste artigo, compartilharemos insights sobre como utilizar a programação em MQL5 para identificar níveis de mercado — diferenciando entre níveis de preço mais fracos e mais fortes. Desenvolveremos completamente um indicador funcional de Força de Suporte e Resistência (SRSI).