English Русский 中文 Español Deutsch 日本語
WebRequest multi-threaded assíncrono em MQL5

WebRequest multi-threaded assíncrono em MQL5

MetaTrader 5Experts | 22 janeiro 2019, 07:38
3 268 3
Stanislav Korotky
Stanislav Korotky

A implementação de algoritmos de negociação geralmente requer a análise de informações de várias fontes externas, em particular da Internet. A MQL5 fornece a função WebRequest para enviar solicitações HTTP, mas, infelizmente, ela tem uma falha importante. Esta função é síncrona e, portanto, bloqueia EAs durante toda a duração da solicitação. Lembre-se de que, para cada EA no MetaTrader 5, há uma única thread que executa sequencialmente as chamadas das funções API existentes no código e que inicia manipuladores para eventos de entrada (como ticks, alterações do livro de ofertas no BookEvent, temporizador, negócios, eventos gráficos, etc.). Apenas um trecho de código é executado por vez, enquanto os outros aguardam sua vez, até que o trecho atual retorna o controle ao kernel.

Por exemplo, se um EA tiver que processar novos ticks em tempo real e verificar periodicamente notícias econômicas num ou vários sites, é impossível cumprir os dois requisitos sem causar algum prejuízo. Depois que o WebRequest é executado no código, o EA permanece congelado na string com a chamada de função, enquanto os eventos sobre novos ticks são ignorados. Mesmo considerando que depois os ticks perdidos podem ser lidos usando a função CopyTicks, o momento da toma de decisão pode ser perdido. Veja como esta situação é ilustrada usando o diagrama de sequência UML:

Diagrama de sequência de processamento de eventos com um código de bloqueio numa thread

Fig.1 Diagrama de sequência de processamento de eventos com um código de bloqueio numa thread

Por essa razão, é necessário criar uma ferramenta para execução assíncrona e não-bloqueante de solicitações HTTP, um tipo de WebRequestAsync. Obviamente, isso requer algo para obter threads adicionais. No MetaTrader 5, isso é feito com mais facilidade executando EAs assistentes para os quais se pode enviar informações sobre solicitações HTTP, chamar o WebRequest e, após algum tempo, receber resultados. Enquanto a solicitação é processada num EA assistente desse tipo, nosso EA principal permanece disponível para ações imediatas e interativas. Para tal caso, o diagrama de sequência UML pode ser assim:

Diagrama de sequência com delegação de processamento assíncrono de evento para outras threads

Fig.2 Diagrama de sequência com delegação de processamento assíncrono de evento para outras threads


1. Desenho

Como é sabido, no MetaTrader, cada EA deve trabalhar em gráficos separados. Assim, a criação de EAs assistentes exige que se pré-criem gráficos especificamente para eles. Fazer isso manualmente seria um pouco complicado, por isso, faz sentido delegar todas as operações de rotina a um gerente especial, bem como a um EA que gerencie um conjunto de gráficos e EAs assistentes, além de fornecer um único ponto de entrada para registrar novas solicitações de programas do cliente. Em certo sentido, essa arquitetura pode ser chamada de três níveis, semelhante à arquitetura cliente/servidor, em que o servidor é um EA gerente:

<img src="https://c.mql5.com/2/34/multiweb4mql.png" title="Arquitetura da biblioteca multiweb: código MQL do cliente servidor (gerente do pool de assistentes) EAs assistentes" alt="Arquitetura da biblioteca multiweb: código MQL do cliente servidor (gerente do pool de assistentes) EAs assistentes" style="vertical-align:middle;" width="695" height="364">

Fig. 3 Arquitetura da biblioteca multiweb: código MQL do cliente <-> servidor (gerente do pool de assistentes) <-> EAs assistentes

Mas, para simplificar, o gerente e o EA assistente podem ser implementados como um mesmo código (programa). Um dos dois papéis de um EA universal - um gerente ou assistente - deve ser determinado por prioridade. A primeira instância inciada se manifestará como um gerente, abrirá gráficos auxiliares e executará um número específico de si mesma neles, mas desempenhando a função de assistente.

O que exatamente e como o cliente, o gerente e os assistentes devem se transferir entre si? Para entender isso, analisemos a função WebRequest.

Como se sabe, no MetaTrader 5 existem dois tipos de função WebRequest, e vamos considerar a segunda como a mais universal.

int WebRequest
( 
  const string      method,           // método HTTP 
  const string      url,              // endereço url 
  const string      headers,          // cabeçalhos  
  int               timeout,          // tempo de espera esgotado 
  const char        &data[],          // matriz do corpo da mensagem HTTP 
  char              &result[],        // matriz com dados de resposta do servidor 
  string            &result_headers   // cabeçalhos de resposta do servidor 
);

Os primeiros 5 parâmetros são de entrada, eles são transferidos do código de chamada para o kernel e determinam a essência da solicitação. Os últimos dois parâmetros são de entrada, eles são transferidos do kernel para o código de chamada e contêm o resultado da solicitação. Obviamente, converter essa função em assíncrona requer dividi-la em dois componentes: inicialização de solicitação e obtenção de resultados:

int WebRequestAsync
( 
  const string      method,           // método HTTP 
  const string      url,              // endereço url 
  const string      headers,          // cabeçalhos  
  int               timeout,          // tempo de espera esgotado 
  const char        &data[],          // matriz do corpo da mensagem HTTP 
);

int WebRequestAsyncResult
( 
  char              &result[],        // matriz com dados de resposta do servidor 
  string            &result_headers   // cabeçalhos de resposta do servidor 
);

Os nomes e protótipos das funções são condicionais. Na verdade, precisamos transferir essas informações entre diferentes programas MQL, e chamadas de função normais não são adequadas para isso. Para interação de programas MQL entre si, o MetaTrader 5 fornece um sistema de troca de eventos personalizados. Nós vamos usá-lo. É importante notar que a troca de eventos é baseada na identificação do destinatário usando Chartid — é único para cada gráfico. Em cada gráfico há apenas um EA, mas, no caso de indicadores, não existe essa limitação e, portanto, o usuário deve tomar cuidado para que num gráfico não haja mais de um indicador interagindo com o gerente.

Para que a transferência de dados funcione, é preciso de alguma forma empacotar todos os parâmetros das "funções" nos parâmetros dos eventos personalizados. Os parâmetros e resultados da solicitação podem conter quantidades razoavelmente grandes de informações que não se encaixam fisicamente no escopo limitado dos eventos. Por exemplo, mesmo se decidíssemos passar o método HTTP e a URL no parâmetro de string do evento sparam, a limitação a 63 caracteres seria um obstáculo na maioria dos casos de trabalho. Isso significa que o sistema de troca de eventos precisa ser complementado com algum tipo de repositório de informações compartilhado, enquanto as referências de registros neste repositório devem ser enviados Somente nos parâmetros de evento. Felizmente, o MetaTrader 5 fornece esse repositório na forma de recursos personalizados. Em princípio, os recursos criados dinamicamente a partir da MQL são sempre imagens. Mas, na verdade, a imagem é um recipiente de informações binárias onde se pode escrever o que quiser.

Para simplificar a tarefa, usaremos uma solução pronta para escrever e ler dados arbitrários em recursos personalizados — classes de Resource.mqh e ResourceData.mqhdesenvolvidas por fxsaber, um membro da comunidade MQL5.

O link anexado leva à biblioteca TradeTransactions, que não está relacionada ao tópico deste artigo, mas em sua discussão existe um exemplo de armazenamento e troca de dados através de recursos. A biblioteca pode mudar, pois suas versões correspondem ao momento da escrita do artigo e podem diferir das versões atuais do link acima. Também para a conveniência dos leitores, todos os arquivos usados no artigo são anexados no final do artigo. Além disso, as classes de recursos mencionadas usam outra biblioteca - TypeToBytes - e sua versão também é anexada ao artigo.

Não precisamos nos aprofundar na estrutura interna dessas classes auxiliares. O principal é que podemos confiar na classe RESOURCEDATA pronta como uma “caixa preta” e usar seu construtor e algumas funções que necessitamos. Vamos ver isso em mais detalhes depois. Agora voltemos para a elaboração do conceito global.

A sequência de interação de partes da nossa arquitetura é a seguinte:

  1. Para executar uma solicitação da Web de maneira assíncrona, o programa MQL do cliente deve, usando as classes que desenvolvemos, empacotar os parâmetros de solicitação num recurso local e enviar um evento personalizado para o gerente com um referência para o recurso; o recurso é criado dentro do programa do lado do cliente e não é excluído até que os resultados sejam obtidos (quando se tornar desnecessário);
  2. O gerente encontra um EA assistente livre no pool e envia para ele um link de recursos; no entanto, essa instância é marcada como ocupada temporariamente e não pode ser selecionada para futuras solicitações até que a solicitação atual seja processada;
  3. O EA assistente que recebe o evento personalizado descompacta os parâmetros de solicitação da Web a partir do recurso externo do cliente;
  4. O EA assistente chama o WebRequest de bloqueio padrão e aguarda a resposta (cabeçalho e/ou documento da Web);
  5. O EA assistente empacota os resultados da solicitação em seu recurso local e envia um evento personalizado ao gerente com um link para esse recurso;
  6. O gerente encaminha o evento para o cliente e marca o assistente correspondente novamente como livre;
  7. O cliente recebe uma mensagem do gerente e descompacta o resultado da solicitação a partir do recurso externo do assistente;
  8. Cliente e assistente podem excluir seus recursos locais.

Nas etapas 5 e 6, é possível transferir o resultado de forma mais eficiente, devido ao fato de que o EA assistente envia o resultado diretamente para a janela do cliente, contornando o gerente.

As etapas descritas acima estão relacionadas ao estágio principal do processamento de solicitações HTTP, no entanto, nos bastidores, permanece o processo de coordenação das partes desconexas numa arquitetura única. Ele também depende parcialmente dos eventos personalizados.

O elo central da arquitetura - o gerente - deve ser iniciado manualmente, mas basta fazer isso apenas uma vez. Como qualquer outro EA em andamento, ele será automaticamente recuperado com o gráfico depois que o terminal for reiniciado. No terminal, é permitido ter apenas um gerente de solicitações da Web.

O gerente criará o número necessário de janelas auxiliares (será definido nas configurações) e nelas iniciará cópias dele mesmo que “entenderão” que são assistentes graças a um “protocolo” especial (para detalhes, consulte a seção de implementação).

Qualquer assistente informará o gerente de seu fechamento com a ajuda de um evento especial. Isso é necessário para manter uma lista atualizada de assistentes disponíveis no gerente. Por analogia, o gerente notificará os assistentes de seu fechamento e, em resposta, os assistentes pararão de trabalhar e fecharão suas janelas. Os assistentes não fazem sentido sem um gerente e, em qualquer caso, o reinício do gerente recriará os assistentes (por exemplo, se você alterar o número de assistentes nas configurações).

As janelas para assistentes, como os próprios EAs assistentes, devem sempre ser criadas automaticamente a partir do gerente e, portanto, nosso programa deve "limpá-las". Atenção: não se deve iniciar o EA assistente manualmente, uma vez que a definição de parâmetros de entrada que não correspondam à missão do papel do gerente será considerada pelo programa como um erro.

O programa MQL do cliente, ao ser inicializado, deve pesquisar nas janelas do terminal se existe um gerente com ajuda de um programa de distribuição, especificando seu ChartID no parâmetro. Em resposta, o gerente (se houver) deve retornar sua ID de janela para o cliente. Depois disso, o cliente e o gerente podem trocar mensagens de trabalho.

Aqui, talvez, já estão mencionados todos os principais pontos do projeto. É hora de passar para a implementação.


2. Implementação

Para simplificar o desenvolvimento, criaremos um único arquivo de cabeçalho multiweb.mqh, no qual descreveremos todas as classes: algumas delas serão comuns para o cliente e "servidores", enquanto outras serão herdadas, específicas para cada uma dessas funções.

2.1. Classes base (início)

Vamos começar com a classe na qual serão armazenados os recursos, identificadores e variáveis de estado de cada um dos participantes do esquema. Instâncias de classes derivadas dela serão usadas no gerente, nos assistentes e no cliente. No cliente e nos assistentes, esses objetos são necessários principalmente para armazenar os recursos "enviados por referência". Mas, além disso, notamos que no cliente várias instâncias podem ser criadas para executar várias solicitações da Web em paralelo e, portanto, a análise do status das solicitações atuais (pelo menos, um sinal de que o objeto já está funcionando ou não) deve ser usada em clientes totalmente. O gerente usa esses objetos para identificar e rastrear o status dos auxiliares. Esta é a classe base em si:

class WebWorker
{
  protected:
    long chartID;
    bool busy;
    const RESOURCEDATA<uchar> *resource;
    const string prefix;
    
    const RESOURCEDATA<uchar> *allocate()
    {
      release();
      resource = new RESOURCEDATA<uchar>(prefix + (string)chartID);
      return resource;
    }
    
  public:
    WebWorker(const long id, const string p = "WRP_"): chartID(id), busy(false), resource(NULL), prefix("::" + p)
    {
    }

    ~WebWorker()
    {
      release();
    }
    
    long getChartID() const
    {
      return chartID;
    }
    
    bool isBusy() const
    {
      return busy;
    }
    
    string getFullName() const
    {
      return StringSubstr(MQLInfoString(MQL_PROGRAM_PATH), StringLen(TerminalInfoString(TERMINAL_PATH)) + 5) + prefix + (string)chartID;
    }
    
    virtual void release()
    {
      busy = false;
      if(CheckPointer(resource) == POINTER_DYNAMIC) delete resource;
      resource = NULL;
    }

    static void broadcastEvent(ushort msg, long lparam = 0, double dparam = 0.0, string sparam = NULL)
    {
      long currChart = ChartFirst(); 
      while(currChart != -1)
      {
        if(currChart != ChartID())
        {
          EventChartCustom(currChart, msg, lparam, dparam, sparam); 
        }
        currChart = ChartNext(currChart);
      }
    }
};

Variáveis:

  • chartID — identificador do gráfico ao qual o programa MQL está anexado;
  • busy — sinal de que essa instância está ocupada processando uma solicitação da Web;
  • resource — objeto do recurso - objeto de armazenamento de dados aleatórios; a classe RESOURCEDATA é obtida de ResourceData.mqh;
  • prefix — prefixo exclusivo para cada função; o prefixo é usado no nome dos recursos; num cliente específico, recomenda-se fazer sua própria configuração única, como será mostrado posteriormente; EAs assistentes usam o prefixo padrão "WRR_" (abreviado da Web Request Result);

O método allocate, que será usado em classes derivadas, cria um objeto de recurso do tipo RESOURCEDATA<uchar> na variável de recurso e no nome do recurso, além do prefixo, o identificador de gráfico também participa. A liberação de recursos pode ser executada usando o método release.

De particular interesse é o método getFullName, que retorna o nome completo do recurso, incluindo o nome e o diretório do programa MQL atual. É com esse nome que se pode acessar recursos de programas de terceiros (mas apenas para leitura). Por exemplo, se o EA multiweb.mq5 for colocado no diretório MQL5\Experts e for iniciado no gráfico com o identificador 129912254742671346, o recurso receberá o nome completo "\Experts\multiweb.ex5::WRR_129912254742671346". Vamos transmitir esse tipo de string como um link de recursos com ajuda do parâmetro de string sparam de eventos personalizados.

O método broadcastEvent estático, que envia mensagens para todas as janelas, será usado no futuro para localizar o gerente.

Para trabalhar com a solicitação e o recurso associado a ela no programa cliente, definimos a classe ClientWebWorker, derivada de WebWorker (doravante o código é abreviado, as versões completas estão nos arquivos anexados).

class ClientWebWorker : public WebWorker
{
  protected:
    string _method;
    string _url;
    
  public:
    ClientWebWorker(const long id, const string p = "WRP_"): WebWorker(id, p)
    {
    }

    string getMethod() const
    {
      return _method;
    }

    string getURL() const
    {
      return _url;
    }
    
    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      // allocate()? and what's next?
      ...
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ": Reading result ", resname);
      
      ...
    }
};

Em primeiro lugar, observe que o método request é a implementação real da etapa 1, discutida acima. Aqui é realizado o envio da solicitação da web ao gerente, enquanto a declaração do método segue o protótipo do WebRequestAsync hipotético. O método estático receiveResult executa a ação inversa da etapa 7. Como o primeiro parâmetro de entrada resname, ele recebe o nome completo do recurso externo no qual são armazenados os resultados da solicitação, enquanto a matriz initiator, os cabeçalhos e o texto de bytes devem ser preenchidos dentro do método com dados descompactados a partir do recurso.

Você pode perguntar o que faz initiator. A resposta é muito simples. Como todas as nossas "chamadas" agora serão assíncronas (e a ordem de sua execução não é garantida), você deve ser capaz de corresponder o resultado com a solicitação enviada anteriormente. Portanto, os EAs assistentes incluirão em seus recursos de resposta não apenas dados obtidos da Internet, mas também o nome completo do recurso do cliente original para o qual a solicitação foi iniciada. Após a descompactação, o nome estará no parâmetro initiator e poderá ser usado para associar o resultado à solicitação correspondente.

O método receiveResult é estático, porque não usa nenhuma variável de objeto - todos os resultados são retornados ao código de chamada via parâmetros.

Em ambos os métodos, há reticências onde é necessário empacotar e descompactar os dados de e para os recursos. Nós vamos lidar com esse problema na próxima seção.


2.2. Empacotamento de solicitações e resultados de solicitações para recursos

Conforme nos lembramos, os recursos devem ser processados no nível inferior usando a classe RESOURCEDATA. Ela é padrão, isto é, aceita um parâmetro com o tipo de dados que iremos escrever e ler do recurso. Como nossos dados também contêm strings, como unidade de armazenamento é lógico escolher o mais pequeno - tipo uchar. Assim, o contêiner de dados será um objeto da classe RESOURCEDATA<uchar>. Ao criar um recurso, seu construtor especifica o nome exclusivo name (para o programa):

RESOURCEDATA<uchar>(const string name)

É este nome, complementado na frente com o nome do programa, que podemos transmitir em eventos de usuários, para que outros programas MQL possam acessar o mesmo recurso. Observe que todos os outros programas, exceto aquele em que o recurso foi criado, têm acesso de leitura somente.

Os dados são gravados no recurso usando o operador de atribuição sobrecarregado:

void operator=(const uchar &array[]) const

onde o array é um tipo de matriz que temos que preparar.

A leitura de dados de um recurso é realizada usando a função:

int Get(uchar &array[]) const

Aqui, o array é o parâmetro de saída onde o conteúdo da matriz original será colocado.

Agora vamos voltar para a faceta prática do uso de recursos para transferir dados sobre solicitações HTTP e seus resultados. Criamos uma camada de classe entre os recursos e nosso código principal - ResourceMediator. No lado do cliente, ele empacotará os parâmetros method, url, headers, timeout, data numa matriz de matriz de bytes e, em seguida, gravará no recurso, enquanto, no lado do servidor, descompactará os parâmetros do recurso. Da mesma forma, no lado do servidor, essa classe empacotará os parâmetros result e result_headers numa matriz de bytes e gravará num recurso, em seguida, lerá como uma matriz e irá descompactá-la no lado do cliente.

O construtor ResourceMediator usa um ponteiro para o recurso RESOURCEDATA, que será processado dentro dos métodos. Além disso, o ResourceMediator contém estruturas de suporte para armazenar meta-informações sobre dados. De fato, ao empacotar e descompactar recursos, além dos dados em si, precisaremos de um certo cabeçalho contendo os tamanhos de todos os campos.

Por exemplo, se simplesmente usarmos a função StringToCharArray para converter uma URL numa matriz de bytes, ao executar a operação inversa usando CharArrayToString, precisamos definir o comprimento da matriz. Caso contrário, se não for especificado, não apenas os bytes da URL serão lidos da matriz, mas também os campos com o cabeçalho a seguir. Deixe-me lembrá-lo de que adicionamos todos os dados numa única matriz antes de gravar no recurso. Meta-informação sobre o comprimento dos campos também deve ser convertida numa sequência de bytes, para o qual são usadas uniões (union).

#define LEADSIZE (sizeof(int)*5) // 5 fields in web-request

class ResourceMediator
{
  private:
    const RESOURCEDATA<uchar> *resource; // underlying asset
    
    // meta-data in header is represented as 5 ints `lengths` and/or byte array `sizes`
    union lead
    {
      struct _l
      {
        int m; // method
        int u; // url
        int h; // headers
        int t; // timeout
        int b; // body
      }
      lengths;
      
      uchar sizes[LEADSIZE];
      
      int total()
      {
        return lengths.m + lengths.u + lengths.h + lengths.t + lengths.b;
      }
    }
    metadata;
  
    // represent int as byte array and vice versa
    union _s
    {
      int x;
      uchar b[sizeof(int)];
    }
    int2chars;
    
    
  public:
    ResourceMediator(const RESOURCEDATA<uchar> *r): resource(r)
    {
    }
    
    void packRequest(const string method, const string url, const string headers, const int timeout, const uchar &body[])
    {
      // fill metadata with parameters data lengths
      metadata.lengths.m = StringLen(method) + 1;
      metadata.lengths.u = StringLen(url) + 1;
      metadata.lengths.h = StringLen(headers) + 1;
      metadata.lengths.t = sizeof(int);
      metadata.lengths.b = ArraySize(body);
      
      // allocate resulting array to fit metadata plus parameters data
      uchar data[];
      ArrayResize(data, LEADSIZE + metadata.total());
      
      // put metadata as byte array at the beginning of the array
      ArrayCopy(data, metadata.sizes);
      
      // put all data fields into the array, one by one
      int cursor = LEADSIZE;
      uchar temp[];
      StringToCharArray(method, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor += metadata.lengths.m;
      
      StringToCharArray(url, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor += metadata.lengths.u;
      
      StringToCharArray(headers, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor += metadata.lengths.h;
      
      int2chars.x = timeout;
      ArrayCopy(data, int2chars.b, cursor);
      cursor += metadata.lengths.t;
      
      ArrayCopy(data, body, cursor);
      
      // store the array in the resource
      resource = data;
    }
    
    ...

O método packRequest, em primeiro lugar, grava os tamanhos de todos os campos na estrutura metadata. Em seguida, o conteúdo dessa estrutura é copiado como uma matriz de bytes para o início da matriz data, que será posteriormente colocada no recurso. O tamanho da matriz de dados é reservado com base no comprimento total de todos os campos e no tamanho da própria estrutura com metadados. Parâmetros do tipo string são convertidos em matrizes usando StringToCharArray e copiados na matriz resultante no deslocamento correspondente, que é mantido atualizado na variável de cursor o tempo todo. O parâmetro timeout é convertido numa matriz de caracteres usando a união int2chars. O parâmetro body é copiado para a matriz como está, já que ele já representa uma matriz do tipo requerido. Finalmente, a transferência direta do conteúdo da matriz comum para o recurso é realizada numa string (operador = sobrecarregado na classe RESOURCEDATA):

      resource = data;

A operação inversa para recuperar os parâmetros de solicitação do recurso é executada no método unpackRequest.

    void unpackRequest(string &method, string &url, string &headers, int &timeout, uchar &body[])
    {
      uchar array[];
      // fill array with data from resource  
      int n = resource.Get(array);
      Print(ChartID(), ": Got ", n, " bytes in request");
      
      // read metadata from the array
      ArrayCopy(metadata.sizes, array, 0, 0, LEADSIZE);
      int cursor = LEADSIZE;

      // read all data fields, one by one      
      method = CharArrayToString(array, cursor, metadata.lengths.m);
      cursor += metadata.lengths.m;
      url = CharArrayToString(array, cursor, metadata.lengths.u);
      cursor += metadata.lengths.u;
      headers = CharArrayToString(array, cursor, metadata.lengths.h);
      cursor += metadata.lengths.h;
      
      ArrayCopy(int2chars.b, array, 0, cursor, metadata.lengths.t);
      timeout = int2chars.x;
      cursor += metadata.lengths.t;
      
      if(metadata.lengths.b > 0)
      {
        ArrayCopy(body, array, 0, cursor, metadata.lengths.b);
      }
    }
    
    ...

Aqui o trabalho principal é executado pela string com a chamada resource.Get(array). Em seguida, a partir da matriz array, os bytes de metadados são lidos em estágios e, em sua base, todos os campos subsequentes.

Da mesma forma, os resultados da execução da solicitação são empacotados e descompactados usando os métodos packResponse e unpackResponse (o código completo está anexado).

    void packResponse(const string source, const uchar &result[], const string &result_headers);
    void unpackResponse(uchar &initiator[], uchar &headers[], uchar &text[]);

Agora podemos voltar ao código fonte ClientWebWorker e concluir os métodos request e receiveResult.

class ClientWebWorker : public WebWorker
{
    ...

    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      ResourceMediator mediator(allocate());
      mediator.packRequest(method, url, headers, timeout, body);
    
      busy = EventChartCustom(managerChartID, 0 /* TODO: specific message */, chartID, 0.0, getFullName());
      return busy;
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ": Reading result ", resname);
      const RESOURCEDATA<uchar> resource(resname);
      ResourceMediator mediator(&resource);
      mediator.unpackResponse(initiator, headers, text);
    }
};

Eles se tornaram bastante simples graças ao uso da classe ResourceMediator, que assumiu todo o trabalho de rotina.

Resta a pergunta: quem e quando chamará os métodos WebWorker e como obter os valores de alguns parâmetros de serviço, como managerChartID no método request? Olhando para o futuro, respondemos que é lógico confiar o gerenciamento de todos os objetos das classes WebWorker a algumas classes de nível mais alto que manteriam listas atuais de objetos e trocariam mensagens entre os programas "em nome" dos objetos, incluindo mensagens de busca de gerente. Mas antes de passarmos para este novo nível, é necessário concluir uma preparação semelhante no "back-end".


2.3. Classes base (continuação)

Assim como no lado do cliente, a classe clientWeber lida com solicitações assíncronas e, no lado do servidor, no gerente, declaramos nosso próprio derivado de WebWorker para essa finalidade.

class ServerWebWorker : public WebWorker
{
  public:
    ServerWebWorker(const long id, const string p = "WRP_"): WebWorker(id, p)
    {
    }
    
    bool transfer(const string resname, const long clientChartID)
    {
      // respond to the client with `clientChartID` that the task in `resname` was accepted
      // and pass the task to this specific worker identified by `chartID` 
      busy = EventChartCustom(clientChartID, TO_MSG(MSG_ACCEPTED), chartID, 0.0, resname)
          && EventChartCustom(chartID, TO_MSG(MSG_WEB), clientChartID, 0.0, resname);
      return busy;
    }
    
    void receive(const string source, const uchar &result[], const string &result_headers)
    {
      ResourceMediator mediator(allocate());
      mediator.packResponse(source, result, result_headers);
    }
};

O método transfer delega o processamento da solicitação a uma instância específica do EA assistente de acordo com a etapa 2 na sequência de interação geral. O parâmetro resname é o nome do recurso recebido do cliente e o clientChartID é o identificador da janela do cliente. Todos esses parâmetros são obtidos por nós de eventos personalizados. Os eventos de usuário em si, em particular MSG_WEB, serão descritos abaixo.

O método receive cria um recurso local no objeto WebWorker atual (chamada de allocate) e grava o nome do recurso inicial do iniciador de solicitação (origem), bem como dados recebidos da Internet (result) e cabeçalhos HTTP (result_headers) usando o objeto mediator da classe ResourceMediator. Isso faz parte da sequência 5 da sequência geral.

Portanto, definimos as classes WebWorker para o cliente e para o "servidor". Em ambos os casos, esses objetos provavelmente serão criados em grandes quantidades. Por exemplo, um cliente pode baixar vários documentos de uma só vez, enquanto, do lado do gerente, é inicialmente bom distribuir um número suficiente de assistentes, pois os pedidos podem vir de vários clientes simultaneamente. Para realizar uma matriz de objetos, definimos uma classe base especial WebWorkersPool. Vamos torná-la padrão, porque o tipo de objetos armazenados será diferente no cliente e no "servidor", isto é, ClientWebWorker e ServerWebWorker, respectivamente.

template<typename T>
class WebWorkersPool
{
  protected:
    T *workers[];
    
  public:
    WebWorkersPool() {}
    
    WebWorkersPool(const uint size)
    {
      // allocate workers; in clients they are used to store request parameters in resources
      ArrayResize(workers, size);
      for(int i = 0; i < ArraySize(workers); i++)
      {
        workers[i] = NULL;
      }
    }
    
    ~WebWorkersPool()
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(CheckPointer(workers[i]) == POINTER_DYNAMIC) delete workers[i];
      }
    }
    
    int size() const
    {
      return ArraySize(workers);
    }
    
    void operator<<(T *worker)
    {
      const int n = ArraySize(workers);
      ArrayResize(workers, n + 1);
      workers[n] = worker;
    }
    
    T *findWorker(const string resname) const
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getFullName() == resname)
          {
            return workers[i];
          }
        }
      }
      return NULL;
    }
    
    T *getIdleWorker() const
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(ChartPeriod(workers[i].getChartID()) > 0) // check if exist
          {
            if(!workers[i].isBusy())
            {
              return workers[i];
            }
          }
        }
      }
      return NULL;
    }
    
    T *findWorker(const long id) const
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getChartID() == id)
          {
            return workers[i];
          }
        }
      }
      return NULL;
    }
    
    bool revoke(const long id)
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getChartID() == id)
          {
            if(CheckPointer(workers[i]) == POINTER_DYNAMIC) delete workers[i];
            workers[i] = NULL;
            return true;
          }
        }
      }
      return false;
    }
    
    int available() const
    {
      int count = 0;
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          count++;
        }
      }
      return count;
    }
    
    T *operator[](int i) const
    {
      return workers[i];
    }
    
};

A essência dos métodos é simples. O construtor e o destrutor selecionam e liberam uma matriz de manipuladores de um determinado tamanho. O grupo de métodos findWorker e getIdleWorker procura objetos numa matriz usando vários critérios. O operador operator<< permite adicionar dinamicamente objetos, enquanto o método de revoke - excluir dinamicamente.

O pool de manipuladores do lado do cliente deve ter alguma especificidade (em particular, no que diz respeito ao tratamento de eventos) e, portanto, estenderemos a classe base usando o ClientWebWorkersPool derivado.

template<typename T>
class ClientWebWorkersPool: public WebWorkersPool<T>
{
  protected:
    long   managerChartID;
    short  managerPoolSize;
    string name;
    
  public:
    ClientWebWorkersPool(const uint size, const string prefix): WebWorkersPool(size)
    {
      name = prefix;
      // try to find WebRequest manager chart
      WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID());
    }
    
    bool WebRequestAsync(const string method, const string url, const string headers, int timeout, const char &data[])
    {
      T *worker = getIdleWorker();
      if(worker != NULL)
      {
        return worker.request(method, url, headers, timeout, data, managerChartID);
      }
      return false;
    }
    
    void onChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
    {
      if(MSG(id) == MSG_DONE) // async request is completed with result or error
      {
        Print(ChartID(), ": Result code ", (long)dparam);
    
        if(sparam != NULL)
        {
          // read data from the resource with name in sparam
          uchar initiator[], headers[], text[];
          ClientWebWorker::receiveResult(sparam, initiator, headers, text);
          string resname = CharArrayToString(initiator);
          
          T *worker = findWorker(resname);
          if(worker != NULL)
          {
            worker.onResult((long)dparam, headers, text);
            worker.release();
          }
        }
      }
      
      ...
      
      else
      if(MSG(id) == MSG_HELLO) // manager is found as a result of MSG_DISCOVER broadcast
      {
        if(managerChartID == 0 && lparam != 0)
        {
          if(ChartPeriod(lparam) > 0)
          {
            managerChartID = lparam;
            managerPoolSize = (short)dparam;
            for(int i = 0; i < ArraySize(workers); i++)
            {
              workers[i] = new T(ChartID(), name + (string)(i + 1) + "_");
            }
          }
        }
      }
    }
    
    bool isManagerBound() const
    {
      return managerChartID != 0;
    }
};

Variáveis:

  • managerChartID — identificador da janela onde o gerente de trabalho é encontrado;
  • managerPoolSize — tamanho inicial da matriz de objetos manipuladores;
  • name — prefixo geral para recursos em todos os objetos do pool;


2.4. Sistema de mensagens

No construtor ClientWebWorkersPool, vemos uma chamada de WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()), que despacha o evento MSG_DISCOVER para todas as janelas, passando o identificador da janela atual no parâmetro de evento. MSG_DISCOVER — tipo de valor reservado, quer dizer, ele junto com outros tipos de mensagens, que nossos programas trocarão, devem ser definidos no início do mesmo arquivo de cabeçalho.

#define MSG_DEINIT   1 // tear down (manager <-> worker)
#define MSG_WEB      2 // start request (client -> manager -> worker)
#define MSG_DONE     3 // request is completed (worker -> client, worker -> manager)
#define MSG_ERROR    4 // request has failed (manager -> client, worker -> client)
#define MSG_DISCOVER 5 // find the manager (client -> manager)
#define MSG_ACCEPTED 6 // request is in progress (manager -> client)
#define MSG_HELLO    7 // the manager is found (manager -> client)

Nos comentários é indicada a direção na qual é enviada uma mensagem.

A macro TO_MSG é projetada para transformar os identificadores listados em códigos de eventos reais em relação a um valor base arbitrário, selecionável pelo usuário. Vamos recebê-lo através do parâmetro de entrada MessageBroadcast.

sinput uint MessageBroadcast = 1;
 
#define TO_MSG(X) ((ushort)(MessageBroadcast + X))

Essa abordagem permite, alterando o valor base, mover todos os eventos em qualquer intervalo livre. Lembre-se de que os próprios eventos podem ser usados no terminal e em outros programas e, portanto, é importante evitar colisões.

O parâmetro de entrada MessageBroadcast aparecerá em todos os nossos programas MQL incluindo o arquivo multiweb.mqh, ou seja, nos clientes e no gerente. Ao iniciar o gerente e os clientes, deve-se especificar o mesmo valor de MessageBroadcast.

Voltemos para a classe ClientWebWorkersPool. O método onChartEvent ocupa um lugar especial. Ele será chamado a partir do manipulador de eventos padrão OnChartEvent. O tipo de evento é passado no parâmetro id. Como recebemos códigos do sistema com base no valor base selecionado, para convertê-los de volta no intervalo MSG _***, usamos a macro MSG "espelhado":

#define MSG(x) (x - MessageBroadcast - CHARTEVENT_CUSTOM)

CHARTEVENT_CUSTOM aqui é o começo do intervalo para todos os eventos personalizados no terminal.

Como se pode ver, o método onChartEvent no ClientWebWorkersPool manipula algumas das mensagens acima. Portanto, em resposta ao envio de MSG_DISCOVER, o gerente deve responder com a mensagem MSG_HELLO. Nesse caso, o parâmetro lparam passa o identificador da janela do gerente, enquanto o parâmetro dparam para referência exibe o número de assistentes disponíveis. Quando o gerente é detectado, o pool preenche, até então, a matriz vazia workers com objetos reais do tipo requerido. Nesse caso, o identificador da janela atual é transferido para o construtor do objeto, bem como o nome exclusivo do recurso em cada objeto (ele consiste no prefixo geral name e no número de sequência da matriz).

Depois que o campo managerChartID recebe um valor significativo, será possível enviar solicitações ao gerente. Na classe ClientWebWorker, para fazer isso, é usado o método de solicitação request, e seu uso é mostrado no método WebRequestAsync do pool. O método WebRequestAsync primeiro encontra um objeto manipulador livre usando getIdleWorker e, em seguida, chama worker.request(method, url, headers, timeout, data, managerChartID). Dentro do método request, tivemos um comentário sobre a seleção de um código especial de mensagens para enviar um evento. Agora, depois de considerar o subsistema de eventos, podemos formar a versão final do método ClientWebWorker::request:

class ClientWebWorker : public WebWorker
{
    ...

    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      ResourceMediator mediator(allocate());
      mediator.packRequest(method, url, headers, timeout, body);
    
      busy = EventChartCustom(managerChartID, TO_MSG(MSG_WEB), chartID, 0.0, getFullName());
      return busy;
    }
    
    ...
};

MSG_WEB — mensagem de execução de solicitação da Web. Após recebê-lo, o gerente terá que encontrar um EA assistente livre e transferir para ele o nome do recurso do cliente (sparam) com as características da solicitação, bem como o identificador da janela do cliente chartID (lparam).

O assistente executará diretamente a consulta e retornará os resultados para o cliente usando o evento MSG_DONE (se bem-sucedido) ou o código de erro usando MSG_ERROR (no caso de problemas). O código de resultado (ou erros) é transmitido para o dparam, enquanto o resultado em si é empacotado num recurso localizado no EA assistente, sob o nome passado para sparam. Na ramificação MSG_DONE, vemos como os dados são recuperados do recurso chamando a função ClientWebWorker::receiveResult(sparam, initiator, headers, text) discutida anteriormente. Em seguida, usando o nome do recurso do solicitante, é procurado um objeto manipulador de cliente (findWorker) e são chamados alguns métodos no objeto encontrado:

    T *worker = findWorker(resname);
    if(worker != NULL)
    {
      worker.onResult((long)dparam, headers, text);
      worker.release();
    }

Nós conhecemos o método release, ele libera recursos desnecessários. Mas onResult é algo novo. Na verdade, se você observar o código fonte completo, a classe ClientWebWorker possui duas funções virtuais sem implementação: onResult e onError. Isso faz com que a classe seja abstrata. O código do cliente deve descrever sua classe derivada a partir de ClientWebWorker e fornecer sua implementação. Pelos próprios nomes dos métodos, pode-se entender que onResult é chamado se os resultados forem recebidos com sucesso, enquanto onError é chamado em caso de erro. Isso fornece feedback entre as classes de trabalho de solicitações assíncronas e o código do programa cliente usando-as. Em outras palavras, o programa do lado do cliente não precisa saber nada sobre as mensagens que o kernel usa internamente: todas as interações do código do cliente com a API desenvolvida são executadas pelas ferramentas POO MQL5.

Vamos ver o código fonte do cliente, multiwebclient.mq5.


2.5. EA do cliente

Com base nos dados inseridos pelo usuário, nosso EA de teste enviará várias solicitações por meio da API multiweb. Para fazer isso, deve-se incluir um arquivo de cabeçalho e adicionar parâmetros de entrada:

sinput string Method = "GET";
sinput string URL = "https://google.com/,https://ya.ru,https://www.startpage.com/";
sinput string Headers = "User-Agent: n/a";
sinput int Timeout = 5000;

#include <multiweb.mqh>

Todos os parâmetros são destinados, em última instância, a configurar as solicitações HTTP em andamento. No campo URL, podem-se listar vários endereços separados por vírgulas para avaliar o paralelismo e a velocidade de execução de solicitações. O parâmetro URL é dividido em endereços usando a função StringSplit em OnInit, assim:

int urlsnum;
string urls[];
  
void OnInit()
{
  // get URLs for test requests
  urlsnum = StringSplit(URL, ',', urls);
  ...
}

Além disso, em OnInit, é preciso criar um pool de objetos manipuladores de solicitações (ClientWebWorkersPool), mas para isso devemos primeiro descrever nossa própria classe, que é derivada de ClientWebWorker.

class MyClientWebWorker : public ClientWebWorker
{
  public:
    MyClientWebWorker(const long id, const string p = "WRP_"): ClientWebWorker(id, p)
    {
    }
    
    virtual void onResult(const long code, const uchar &headers[], const uchar &text[]) override
    {
      Print(getMethod(), " ", getURL(), "\nReceived ", ArraySize(headers), " bytes in header, ", ArraySize(text), " bytes in document");
      // uncommenting this leads to potentially bulky logs
      // Print(CharArrayToString(headers));
      // Print(CharArrayToString(text));
    }

    virtual void onError(const long code) override
    {
      Print("WebRequest error code ", code);
    }
};

Na verdade, ela não faz nada, exceto a saída de informações de log sobre status e dados recebidos. Agora pode-se criar um pool de tais objetos no OnInit.

ClientWebWorkersPool<MyClientWebWorker> *pool = NULL;

void OnInit()
{
  ...
  pool = new ClientWebWorkersPool<MyClientWebWorker>(urlsnum, _Symbol + "_" + EnumToString(_Period) + "_");
  Comment("Click the chart to start downloads");
}

Como você pode ver, o pool é parametrizado pela classe MyClientWebWorker, o que torna possível criar nossos objetos a partir do código da biblioteca. O tamanho da matriz é escolhido igual ao número de endereços inseridos. Isso é ideal para fins de demonstração: um número menor significaria uma fila para processar e descrédito da ideia de execução paralela, enquanto um número maior indicaria um desperdício de recursos. Em projetos reais, o tamanho do pool, obviamente, não precisa ser igual ao número de tarefas, mas isso exige uma ligação algorítmica adicional.

O prefixo para recursos é definido como uma combinação do nome do símbolo de trabalho e o período gráfico.

O toque final na inicialização é encontrar a janela do gerente. Como você deve lembrar-se, o próprio pool é uma classeClientWebWorkersPool. Do código do cliente é necessário certificar de que o gerente é encontrado. Para esses propósitos, vamos definir um tempo razoável, durante o qual a mensagem sobre a pesquisa do gerente e resposta devem garantir o cumprimento das metas. Colocamos 5 segundos. Criamos um cronômetro para este tempo:

void OnInit()
{
  ...
  // wait for manager negotiation for 5 seconds maximum
  EventSetTimer(5);
}

No manipulador do temporizador, verificamos se há gerente e exibimos um alerta se a conexão não for estabelecida.

void OnTimer()
{
  // if the manager did not respond during 5 seconds, it seems missing
  EventKillTimer();
  if(!pool.isManagerBound())
  {
    Alert("WebRequest Pool Manager (multiweb) is not running");
  }
}

No manipulador OnDeinit, deve-se excluir o objeto do pool.

void OnDeinit(const int reason)
{
  delete pool;
  Comment("");
}

Para que o pool manipule todas as mensagens de serviço "por nós", incluindo, em primeiro lugar, a pesquisa de gerente, deve-se usar o manipulador de eventos padrão do gráfico OnChartEvent:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) // initiate test requests by simple user action
  {
    ...
  }
  else
  {
    // this handler manages all important messaging behind the scene
    pool.onChartEvent(id, lparam, dparam, sparam);
  }
}

Todos os eventos, exceto CHARTEVENT_CLICK, são enviados ao pool, onde, com base na análise dos códigos de eventos, são executadas as ações correspondentes (o fragmento onChartEvent foi fornecido anteriormente).

O evento CHARTEVENT_CLICK é interativo e é usado diretamente para iniciar o processo de download. No caso mais simples, isso pode ser, por exemplo:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) // initiate test requests by simple user action
  {
    if(pool.isManagerBound())
    {
      uchar Body[];

      for(int i = 0; i < urlsnum; i++)
      {
        pool.WebRequestAsync(Method, urls[i], Headers, Timeout, Body);
      }
    }
    ...

O código completo do exemplo é um pouco mais detalhado, já que a ele foi adicionada a lógica para calcular o tempo de execução e compará-lo com uma chamada sequencial do WebRequest padrão para o mesmo conjunto de endereços.


2.6. EA gerente e EA assistente

De toda a arquitetura, resta a nós considerar apenas a parte "back-end". Devido ao fato de que os mecanismos básicos já estão implementados no arquivo de cabeçalho, o código do gerente e dos assistentes não são tão complicados quanto se possa imaginar.

Em primeiro lugar, lembramos que, na verdade, temos apenas um o EA trabalhando como gerente ou como assistente — o arquivo multiweb.mq5. Como no caso do cliente, incluímos o arquivo de cabeçalho e declaramos os parâmetros de entrada:

sinput uint WebRequestPoolSize = 3;
sinput ulong ManagerChartID = 0;

#include <multiweb.mqh>

WebRequestPoolSize é o número de janelas auxiliares que o gerente deve criar e em que deve executar assistentes.

ManagerChartID é o identificador da janela do gerente. Esse parâmetro faz sentido apenas para a função de assistente e será preenchido pelo gerente quando os auxiliares forem iniciados automaticamente a partir do código fonte. O preenchimento manual de ManagerChartID ao iniciar o gerente é interpretado como um erro.

O algoritmo é construído em torno de duas variáveis globais:

bool manager;
WebWorkersPool<ServerWebWorker> pool;

O sinalizador lógico manager contém uma indicação da função da instância atual do EA. A variável pool é uma matriz de objetos manipuladores de tarefas entrantes. O WebWorkersPool é tipada pela classe ServerWeber já discutida acima. A matriz não é inicializada antecipadamente, porque seu preenchimento depende de sua função.

A primeira instância iniciada recebe função de ser gerente, que é definido em OnInit.

const string GVTEMP = "WRP_GV_TEMP";

int OnInit()
{
  manager = false;
  
  if(!GlobalVariableCheck(GVTEMP))
  {
    // when first instance of multiweb is started, it's treated as manager
    // the global variable is a flag that the manager is present
    if(!GlobalVariableTemp(GVTEMP))
    {
      FAILED(GlobalVariableTemp);
      return INIT_FAILED;
    }
    
    manager = true;
    GlobalVariableSet(GVTEMP, 1);
    Print("WebRequest Pool Manager started in ", ChartID());
  }
  else
  {
    // all next instances of multiweb are workers/helpers
    Print("WebRequest Worker started in ", ChartID(), "; manager in ", ManagerChartID);
  }
  
  // use the timer for delayed instantiation of workers
  EventSetTimer(1);
  return INIT_SUCCEEDED;
}

O EA verifica se há uma variável global especial de terminal. Se não existir, o EA se faz gerente e cria uma variável global. Se a variável já existir é porque há também um gerente e, portanto, essa instância se tornará assistente. Por favor, note que a variável global é temporária, isto é, não será salva quando o terminal for reiniciado. Mas se o gerente for deixado em qualquer gráfico, ele criará uma variável novamente.

Em seguida, é temporizado 1 segundo, já que a inicialização dos gráficos auxiliares geralmente leva alguns segundos, e fazer isso a partir do OnInit não é muito bom. No manipulador de eventos do temporizador, preenchemos o pool:

void OnTimer()
{
  EventKillTimer();
  if(manager)
  {
    if(!instantiateWorkers())
    {
      Alert("Workers not initialized");
    }
    else
    {
      Comment("WebRequest Pool Manager ", ChartID(), "\nWorkers available: ", pool.available());
    }
  }
  else // worker
  {
    // this is used as a host of resource storing response headers and data
    pool << new ServerWebWorker(ChartID(), "WRR_");
  }
}

No caso da função de assistente, à matriz é adicionado um único objeto manipulador ServerWebWerer simplesmente. O caso do gerente é mais complicado e, portanto, levado à função instantiateWorkers. Consideremos isso separadamente.

bool instantiateWorkers()
{
  MqlParam Params[4];
  
  const string path = MQLInfoString(MQL_PROGRAM_PATH);
  const string experts = "\\MQL5\\";
  const int pos = StringFind(path, experts);
  
  // start itself again (in another role as helper EA)
  Params[0].string_value = StringSubstr(path, pos + StringLen(experts));
  
  Params[1].type = TYPE_UINT;
  Params[1].integer_value = 1; // 1 worker inside new helper EA instance for returning results to the manager or client

  Params[2].type = TYPE_LONG;
  Params[2].integer_value = ChartID(); // this chart is the manager

  Params[3].type = TYPE_UINT;
  Params[3].integer_value = MessageBroadcast; // use the same custom event base number
  
  for(uint i = 0; i < WebRequestPoolSize; ++i)
  {
    long chart = ChartOpen(_Symbol, _Period);
    if(chart == 0)
    {
      FAILED(ChartOpen);
      return false;
    }
    if(!EXPERT::Run(chart, Params))
    {
      FAILED(EXPERT::Run);
      return false;
    }
    pool << new ServerWebWorker(chart);
  }
  return true;
}

Esta função usa a biblioteca de terceiros Expert do nosso velho amigo, o membro da comunidade MQL5 fxsaber, portanto, o arquivo de cabeçalho correspondente é adicionado no início do código fonte.

#include <fxsaber\Expert.mqh>

A biblioteca Expert permite criar dinamicamente modelos tpl com os parâmetros dos EAs especificados e aplicá-los aos gráficos indicados, o que na verdade leva à inicialização de EAs. Em nosso caso, os parâmetros de todos os EAs assistentes são os mesmos, por essa razão, sua lista é gerada uma vez antes de criar um número específico de janelas.

No parâmetro 0, é indicado o caminho para o arquivo executável do EA, ou seja, para ele mesmo. Parâmetro 1: WebRequestPoolSize. Em cada assistente, é igual a 1. Lembre-se de que, no assistente, o objeto manipulador é necessário para armazenar o recurso com os resultados da solicitação HTTP. Cada assistente processa a solicitação com um WebRequest de bloqueio, ou seja, no máximo está envolvido um objeto manipulador. Parâmetro 2: identificador da janela do gerente ManagerChartID. Parâmetro 3: valor base para códigos de mensagens (o parâmetro MessageBroadcast é obtido de multiweb.mqh).

Além disso, no ciclo, são criados gráficos vazios com a ajuda de ChartOpen e EA assistentes são executados neles usando EXPERT::Run(chart, Params). Para cada nova janela, um objeto de manipulador ServerWeber(chart) é criado e adicionado ao pool. Observe que no gerente, os objetos manipuladores não são nada mais do que referências aos identificadores de janela dos assistentes e seus status, já que as solicitações HTTP não são executadas no gerente e os recursos não são criados para elas.

O processamento de tarefas recebidas é executado com base nos eventos personalizados no OnChartEvent.

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(MSG(id) == MSG_DISCOVER) // a worker EA on new client chart is initialized and wants to bind to this manager
  {
    if(manager && (lparam != 0))
    {
      // only manager responds with its chart ID, lparam is the client chart ID
      EventChartCustom(lparam, TO_MSG(MSG_HELLO), ChartID(), pool.available(), NULL);
    }
  }
  else
  if(MSG(id) == MSG_WEB) // a client has requested a web download
  {
    if(lparam != 0)
    {
      if(manager)
      {
        // the manager delegates the work to an idle worker
        // lparam is the client chart ID, sparam is the client resource
        if(!transfer(lparam, sparam))
        {
          EventChartCustom(lparam, TO_MSG(MSG_ERROR), ERROR_NO_IDLE_WORKER, 0.0, sparam);
        }
      }
      else
      {
        // the worker does actually process the web request
        startWebRequest(lparam, sparam);
      }
    }
  }
  else
  if(MSG(id) == MSG_DONE) // a worker identified by chart ID in lparam has finished its job
  {
    WebWorker *worker = pool.findWorker(lparam);
    if(worker != NULL)
    {
      // here we're in the manager, and the pool hold stub workers without resources
      // so this release is intended solely to clean up busy state
      worker.release();
    }
  }
}

Em primeiro lugar, em resposta à mensagem MSG_DISCOVER recebida do cliente com o identificador lparam, o gerente responde com a mensagem MSG_HELLO, que indica o identificador de sua janela.

Quando MSG_WEB é recebido, o lparam deve conter o identificador da janela do cliente que enviou a solicitação e o sparam - o nome do recurso com os parâmetros de consulta compactados. Como gerente, o código tenta transferir a tarefa com esses parâmetros para um assistente livre chamando a função transfer (será discutida abaixo) e, assim, transferir o objeto selecionado para o status ocupado ("busy"). Na ausência de assistentes livres, o evento MSG_ERROR é enviado ao cliente com o código ERROR_NO_IDLE_WORKER. Como assistente, deve-se executar diretamente uma solicitação HTTP, que é feita na função startWebRequest.

O evento MSG_DONE entra no gerente a partir do assistente quando ele carrega o documento solicitado. Encontrado o objeto correspondente pelo ID do assistente no lparam, o gerente o remove do status "busy" chamando o método release. Lembre-se de que o assistente envia o resultado de trabalho ao cliente diretamente.

No código fonte completo, pode-se encontrar outro evento MSG_DEINIT, que está intimamente relacionado ao processamento OnDeinit. Sua essência consiste em que os assistentes são notificados da remoção do gerente e, em resposta, descarregam-se e fecham a janela, enquanto o gerente é notificado da remoção do assistente e o exclui do seu pool. Propõe-se lidar com este mecanismo de forma independente.

A função transfer busca um objeto livre e chama seu método transfer (discutido acima).

bool transfer(const long returnChartID, const string resname)
{
  ServerWebWorker *worker = pool.getIdleWorker();
  if(worker == NULL)
  {
    return false;
  }
  return worker.transfer(resname, returnChartID);
}

A função startWebRequest é definida da seguinte forma:

void startWebRequest(const long returnChartID, const string resname)
{
  const RESOURCEDATA<uchar> resource(resname);
  ResourceMediator mediator(&resource);

  string method, url, headers;
  int timeout;
  uchar body[];

  mediator.unpackRequest(method, url, headers, timeout, body);

  char result[];
  string result_headers;
  
  int code = WebRequest(method, url, headers, timeout, body, result, result_headers);
  if(code != -1)
  {
    // create resource with results to pass back to the client via custom event
    ((ServerWebWorker *)pool[0]).receive(resname, result, result_headers);
    // first, send MSG_DONE to the client with resulting resource
    EventChartCustom(returnChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, pool[0].getFullName());
    // second, send MSG_DONE to the manager to set corresponding worker to idle state
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, NULL);
  }
  else
  {
    // error code in dparam
    EventChartCustom(returnChartID, TO_MSG(MSG_ERROR), ERROR_MQL_WEB_REQUEST, (double)GetLastError(), resname);
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)GetLastError(), NULL);
  }
}

Usando o intermediador de recursos ResourceMediator, a função descompacta os parâmetros da solicitação e chama a função MQL WebRequest padrão. Se a última foi executada sem erros MQL, os resultados serão enviados para o cliente. Para fazer isso, eles são compactados num recurso local - chamando o método receive (discutido anteriormente) - e seu nome é transmitido com a mensagem MSG_DONE no parâmetro sparam da função EventChartCustom. Observe que os erros de HTTP (por exemplo, página 404 inválida ou erro de servidor da Web 501) estão aqui, pois o cliente receberá o código HTTP no parâmetro dparam e os cabeçalhos de resposta HTTP no recurso, o que permite analisar a situação.

Se a chamada WebRequest terminar com um erro MQL, ao cliente é enviada a mensagem MSG_ERROR com o código ERROR_MQL_WEB_REQUEST, e o resultado GetLastError será colocado no dparam. Como o recurso local não é preenchido nesse caso, o nome do recurso de origem é transmitido diretamente no parâmetro sparam para que, no lado do cliente, ainda seja possível identificar uma instância específica do objeto processador com o recurso.

Diagrama de classes da biblioteca MultiWeb para chamadas WebRequest assíncronas e paralelas

Fig. 4. Diagrama de classes da biblioteca MultiWeb para chamadas WebRequest assíncronas e paralelas


3. Demonstração e teste

O procedimento para testar o pacote de software implementado pode ser, por exemplo, o seguinte:

Primeiro, devem-se abrir as configurações do terminal e, na guia Experts, inserir na lista de URLs permitidas para WebRequest todos os servidores sejam acessados.

Em seguida, executamos o EA multiweb e definimos 3 assistentes nos parâmetros de entrada. Como resultado, serão abertas 3 novas janelas, nas quais é executado o mesmo EA multiweb, mas desempenhando uma função diferente. O papel do EA é exibido nos comentários no canto superior esquerdo da janela.

Agora iniciamos o EA do cliente multiwebclient em outro gráfico e clicamos uma vez no gráfico. Com as configurações padrão, ele inicia 3 solicitações da Web paralelas e registra o diagnóstico no log, incluindo o tamanho dos dados recebidos e o tempo de execução. Se o parâmetro especial TestSyncRequests estiver configurado como true, além de solicitações da web paralelas por meio do gerente, as solicitações sequenciais das mesmas páginas serão feitas usando o WebRequest padrão. Isso é feito para comparar a velocidade de execução das duas variantes. Como regra, o processamento paralelo é várias vezes mais rápido que o sequencial - de sqrt(N) a N, onde N é o número de assistentes disponíveis.

Abaixo é mostrado log de exemplo.

01:16:50.587    multiweb (EURUSD,H1)    OnInit 129912254742671339
01:16:50.587    multiweb (EURUSD,H1)    WebRequest Pool Manager started in 129912254742671339
01:16:52.345    multiweb (EURUSD,H1)    OnInit 129912254742671345
01:16:52.345    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671345; manager in 129912254742671339
01:16:52.757    multiweb (EURUSD,H1)    OnInit 129912254742671346
01:16:52.757    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671346; manager in 129912254742671339
01:16:53.247    multiweb (EURUSD,H1)    OnInit 129912254742671347
01:16:53.247    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671347; manager in 129912254742671339
01:17:16.029    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862
01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862
01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: Got 64 bytes in request
01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: GET https://google.com/ User-Agent: n/a 5000 
01:17:16.030    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862
01:17:16.030    multiweb (EURUSD,H1)    129912254742671346: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862
01:17:16.030    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862 after 0 retries
01:17:16.031    multiweb (EURUSD,H1)    129912254742671346: Got 60 bytes in request
01:17:16.031    multiweb (EURUSD,H1)    129912254742671346: GET https://ya.ru User-Agent: n/a 5000 
01:17:16.031    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862
01:17:16.031    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862 after 0 retries
01:17:16.031    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862 after 0 retries
01:17:16.031    multiweb (EURUSD,H1)    129912254742671347: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862
01:17:16.032    multiweb (EURUSD,H1)    129912254742671347: Got 72 bytes in request
01:17:16.032    multiweb (EURUSD,H1)    129912254742671347: GET https://www.startpage.com/ User-Agent: n/a 5000 
01:17:16.296    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200
01:17:16.296    multiweb (EURUSD,H1)    Result code from 129912254742671346: 200, now idle
01:17:16.297    multiweb (EURUSD,H1)    129912254742671346: Done in 265ms
01:17:16.297    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671346
01:17:16.300    multiwebclient (GBPJPY,M5)      129560567193673862: Got 16568 bytes in response
01:17:16.300    multiwebclient (GBPJPY,M5)      GET https://ya.ru
01:17:16.300    multiwebclient (GBPJPY,M5)      Received 3704 bytes in header, 12775 bytes in document
01:17:16.715    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200
01:17:16.715    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671347
01:17:16.715    multiweb (EURUSD,H1)    129912254742671347: Done in 686ms
01:17:16.715    multiweb (EURUSD,H1)    Result code from 129912254742671347: 200, now idle
01:17:16.725    multiwebclient (GBPJPY,M5)      129560567193673862: Got 45236 bytes in response
01:17:16.725    multiwebclient (GBPJPY,M5)      GET https://www.startpage.com/
01:17:16.725    multiwebclient (GBPJPY,M5)      Received 822 bytes in header, 44325 bytes in document
01:17:16.900    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200
01:17:16.900    multiweb (EURUSD,H1)    Result code from 129912254742671345: 200, now idle
01:17:16.900    multiweb (EURUSD,H1)    129912254742671345: Done in 873ms
01:17:16.900    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671345
01:17:16.903    multiwebclient (GBPJPY,M5)      129560567193673862: Got 13628 bytes in response
01:17:16.903    multiwebclient (GBPJPY,M5)      GET https://google.com/
01:17:16.903    multiwebclient (GBPJPY,M5)      Received 790 bytes in header, 12747 bytes in document
01:17:16.903    multiwebclient (GBPJPY,M5)      > > > Async WebRequest workers [3] finished 3 tasks in 873ms

Observe que o tempo total de execução de todas as solicitações é igual ao tempo de execução da solicitação mais lenta.

Obviamente, se o gerente definir o número de assistentes como 1, as solicitações serão processadas sequencialmente.


Fim do artigo

Neste artigo, foi analisado um conjunto de classes e EAs prontos para executar solicitações HTTP no modo sem bloqueio. Isso permite receber dados da Internet em várias threads paralelas e aumentar a eficiência de EAs que, além de solicitações HTTP, devem realizar cálculos analíticos em tempo real. Além disso, essa biblioteca pode ser usada não apenas em EA, mas também em indicadores, em que o WebRequest padrão é proibido. Para implementar toda a arquitetura, tivemos que usar uma amplo leque de recursos MQL: envio de eventos de usuários, criação de recursos, abertura de janelas dinamicamente e execução de EAs nelas.

No momento em que este artigo foi escrito, a criação de janelas auxiliares para a inicialização de EAs assistentes é a única opção para solicitações HTTP paralelas, mas a MetaQuotes planeja desenvolver programas MQL em segundo plano - serviços, para os quais a pasta MQL5/Services já está reservada. Quando esta abordagem aparecer no terminal, esta biblioteca poderá provavelmente ser melhorada substituindo as janelas auxiliares por serviços.

Lista de arquivos anexados:

  • MQL5/Include/multiweb.mqh — biblioteca
  • MQL5/Experts/multiweb.mq5 — EA gerente e EA assistente 
  • MQL5/Experts/multiwebclient.mq5 — EA cliente de amostra
  • MQL5/Include/fxsaber/Resource.mqh — classe auxiliar para trabalhar com recursos
  • MQL5/Include/fxsaber/ResourceData.mqh — classe auxiliar para trabalhar com recursos
  • MQL5/Include/fxsaber/Expert.mqh — classe auxiliar para a inicialização de EAs
  • MQL5/Include/TypeToBytes.mqh — biblioteca de conversão de dados

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/5337

Arquivos anexados |
MQL5.zip (17.48 KB)
Últimos Comentários | Ir para discussão (3)
Rodrigo Malacarne
Rodrigo Malacarne | 4 abr 2019 em 00:34

Absolutely amazingly incredible article !!!

Congratulations and thank you, @marketeer !

magnomilk
magnomilk | 23 fev 2022 em 11:10

Really nice article. 

However I get issues when trying to compile with metatrader 5.

Initialize sequence for array expected:

   in template 'const TYPETOBYTES::STRUCT_TYPE<T> TYPETOBYTES::FillBytes(const uchar)' specified with [T=uchar] TypeToBytes.mqh 314 31

I understand that it is an issue initilizing the array. I could try to fix. However, I dont see any report of this issue, thinking whether it is only my self facing the issue.

Thanks for the article anywell, wonderful anyway!



Stanislav Korotky
Stanislav Korotky | 24 fev 2022 em 10:49
magnomilk #:

Really nice article. 

However I get issues when trying to compile with metatrader 5.

Initialize sequence for array expected:

   in template 'const TYPETOBYTES::STRUCT_TYPE<T> TYPETOBYTES::FillBytes(const uchar)' specified with [T=uchar] TypeToBytes.mqh 314 31

I understand that it is an issue initilizing the array. I could try to fix. However, I dont see any report of this issue, thinking whether it is only my self facing the issue.

Thanks for the article anywell, wonderful anyway!



Make sure you're using the latest TypeToBytes library.

Usando OpenCL para testar padrões de candles Usando OpenCL para testar padrões de candles
Neste artigo, estudaremos um algoritmo para criar um testador de modelos de candles, em linguagem OpenCL, no modo "OHLC em M1". Além disso, compararemos sua velocidade com a do testador de estratégia embutido, no modo de otimização rápida e lenta.
Padrões de reversão: Testando o padrão 'topo/fundo duplo' Padrões de reversão: Testando o padrão 'topo/fundo duplo'
Na prática, os traders muitas vezes procuram por pontos de reversão, uma vez que é no momento em que surge a tendência que o preço tem o maior potencial de movimento. É por isso que, na prática da análise técnica, são considerados vários padrões de reversão. Um dos padrões mais famosos e usados é o de 'topo/fundo duplo'. Este artigo apresenta uma opção para detectar padrão algoritmicamente, além disso, nele é testada sua rentabilidade em dados históricos.
Reversão: criemos um ponto de entrada e programemos um algoritmo de negociação manual Reversão: criemos um ponto de entrada e programemos um algoritmo de negociação manual
Este é o último artigo da série sobre estratégia de reversão. Nele, tentaremos resolver um problema que levou a resultados inconsistentes relativamente a testes em artigos anteriores. Adicionalmente, escreveremos e testaremos nosso próprio algoritmo para negociar manualmente usando a estratégia de reversão em qualquer mercado.
Reversão: reduzindo o rebaixamento máximo e testando outros mercados Reversão: reduzindo o rebaixamento máximo e testando outros mercados
Nesse artigo, continuaremos falando sobre reversão; tentaremos reduzir o rebaixamento máximo para um nível aceitável em instrumentos já discutidos; verificaremos, enquanto isso, quão afectado fica o lucro obtido. Adicionalmente, checaremos como funciona a reversão em outros mercados, como o de ações, commodities, índices e ETF, agrário. Atenção, esse artigo tem muitas imagens.