English Русский 中文 Deutsch 日本語 Português
WebRequest multiflujo asincrónico en MQL5

WebRequest multiflujo asincrónico en MQL5

MetaTrader 5Asesores Expertos | 4 febrero 2019, 07:07
1 137 1
Stanislav Korotky
Stanislav Korotky

La implementación de los algoritmos requiere con frecuencia el análisis de información de diferentes fuentes externas, en concreto de Internet. MQL5 ofrece la función WebRequest para enviar solicitudes HTTP al "mundo exterior", pero dicha función, por desgracia, tiene un defecto muy notable. Esta función es sincrónica, y por eso bloquea el funcionamiento del experto durante todo el tiempo de ejecución de la solicitud. Recordemos que para cada experto en MetaTrader 5 se reserva un único flujo, que ejecuta consecutivamente las llamadas disponibles a las funciones API en el código, y que también inicia los manejadores de los eventos entrantes (tales como los ticks, los cambios en la profundidad de mercado de BookEvent, el temporizador, las transacciones comerciales, los eventos del gráfico, etc). En cada momento se ejecuta solo un fragmento de código, mientras que las demás "tareas" esperan su momento en las colas, hasta que el fragmento actual no retorne el control al núcleo.

Por ejemplo, si el experto debe procesar nuevos ticks en tiempo real, y además de ello, debe comprobar periódicamente las noticias económicas en uno o varios sitios web, no resultará posible ejecutar ambas misiones sin perjuicio alguno para una u otra. En cuanto el WebRequest se ejecuta en el código, el experto se queda "colgado" en la línea con la llamada de la función, y el evento sobre los nuevos ticks se omitirá. Incluso teniendo en cuenta que los ticks omitidos se pueden leer después con la ayuda de la función CopyTicks, el momento de la toma decisiones ya se habrá dejado escapar. Así, esta situación se ilustra con la ayuda de un diagrama UML de secuencialidad:

Diagrama de secuencia de procesamiento de eventos con código de bloqueo en un flujo

Fig.1 Diagrama de secuencia de procesamiento de eventos con código de bloqueo en un flujo

Por este motivo, sería deseable crear una herramienta para la ejecución asincrónica y no bloqueante de solicitudes HTTP, una especie de WebRequestAsync. Obviamente, para ello necesitaremos conseguir flujos adicionales. En MetaTrader 5 es más sencillo de hacer, iniciando algunos expertos auxiliares en los que se puede enviar información sobre las solicitudes HTTP, llamando allí un WebRequest y obteniendo los resultados como respuesta transcurrido un tiempo. Mientras la solicitud se procesa en el experto auxiliar, nuestro experto principal permanece accesible para las acciones operativas e interactivas. Para este caso, el diagrama UML de secuencialidad puede tener el siguiente aspecto:

Diagrama de secuencialidad con delegación de procesmiento asincrónico de eventos en otros flujos

Fig.2 Diagrama de secuencialidad con delegación de procesmiento asincrónico de eventos en otros flujos


1. Diseño

Como ya sabemos, en MetaTrader, cada experto debe trabajar en un gráfico aparte. De esta forma, la creación de los expertos auxiliares requiere crear gráficos para ellos preliminarmente. Hacer esto de forma manual resultaría incómodo. Por eso, tiene sentido delegar todas las operaciones rutinarias en un gestor especial: otro experto que gestione el conjunto de gráficos y expertos auxiliares, y que además prorcione un punto de entrada único para el registro de las nuevas solicitudes de los programas de cliente. En cierto sentido, esta estructura se puede denominar de 3 niveles, y se asemeja a la arquitectura del servidor-cliente, donde el experto-gestor actúa como servidor:

Arquitectura de la biblioteca multiweb: código MQL de cliente - servidor (gestor del conjunto de ayudantes) - expertos-ayudantes

Fig. 3 Arquitectura de la biblioteca multiweb: código MQL de cliente - servidor (gestor del conjunto de ayudantes) - expertos-ayudantes

Para una mayor sencillez, el gestor y el experto auxiliar pueden implementarse en forma de un mismo código (programa). Uno de los papeles de este experto "universal" — gestor o ayudante — será determinado por prioridad. El primer ejemplar iniciado se declara a sí mismo gestor, abre los gráficos auxiliares e inicia el número establecido de "sí mismos", pero ya como ayudantes.

¿Qué precisamente y cómo deberán transmitir entre sí el cliente, el gestor y los ayudantes? Para comprenderlo, vamos a analizar la función WebRequest.

Como sabemos, en MetaTrader 5 existen dos variantes de la función WebRequest, y vamos a analizar la segunda de ellas como la más universal.

int WebRequest
( 
  const string      method,           // método HTTP 
  const string      url,              // dirección URL 
  const string      headers,          // encabezados  
  int               timeout,          // tiempo de espera agotado 
  const char        &data[],          // matriz del cuerpo del mensaje HTTP 
  char              &result[],        // matriz con los datos de respuesta del servidor 
  string            &result_headers   // encabezados de respuesta del servidor 
);

Los primeros 5 parámetros son de entrada, se transmiten al núcleo desde el código llamado y definen la esencia de la solicitud. Los últimos 2 parámetros son de salida, se transmiten al código llamado desde el núcleo y contienen el resultado. Resulta obvio que la transformación de esta función en asincrónica exige de hecho su división en 2 componentes: la inicialización de la solicitud y la obtención de los resultados:

int WebRequestAsync
( 
  const string      method,           // método HTTP 
  const string      url,              // dirección URL 
  const string      headers,          // encabezados  
  int               timeout,          // tiempo de espera agotado 
  const char        &data[],          // matriz del cuerpo del mensaje HTTP 
);

int WebRequestAsyncResult
( 
  char              &result[],        // matriz con los datos de respuesta del servidor 
  string            &result_headers   // encabezados de respuesta del servidor 
);

Los nombres y los prototipos de las funciones son condicionales. En realidad, debemos transmitir esta información entre diversos programas MQL, y las llamadas normales de las funciones no convienen para ello. Para que los programas MQL se "relacionen" entre sí, en MetaTrader 5 existe un sistema de intercambio con eventos personalizados. Ese sistema vamos a utilizar precisamente. Es importante notar que el intercambio de eventos se realiza usando la identificación del usuario con la ayuda de ChartID, que es única para cada gráfico. Solo puede haber un experto en el gráfico, pero en el caso de los indicadores no existe esa limitación, y por eso el usuario debe preocuparse por sí mismo de que en cada gráfico no haya más de un indicador que interactúe con el gestor.

Para que la transmisión de datos funcione, es necesario de alguna manera empaquetar todos los parámetros de las "funciones" en los parámetros de los eventos informáticos. Tanto los parámetros de la solicitud como los resultados pueden contener volúmenes de información bastante considerables, que no cabrán físicamente en el marco de los eventos. Por ejemplo, incluso si decidiéramos transmitir un método HTTP y un URL en el parámetro de línea de evento sparam, la limitación de la longitud de 63 caracteres supondría un obstáculo en la mayoría de los casos de trabajo. Esto significa que el sistema de intercambio de eventos se debe complementar con un repositorio de información compartido, transmitiendo en los parámetros de los eventos solo los enlaces a las entradas en dicho repositorio. Por fortuna, MetaTrader 5 ofrece un repositorio así en forma de recursos personalizados. En principio, los recursos creados dinámicamente desde MQL son siempre imágenes. Pero, en esencia, una imagen es un contendor con información binaria, donde se puede registrar todo lo que uno desee.

Para simplificar la tarea, usaremos una solución preparada para el registro y lectura de datos aleatorios en los recursos personalizados, las clases Resource.mqh y ResourceData.mqh, desarrolladas por el miembro de la comunidad MQL5, fxsaber.

En el enlace adjunto se ubica la fuente: la biblioteca TradeTransactions, que no tiene relación con el tema de este artículo, si bien en la discusión sobre la biblioteca existe un ejemplo de guardado e intercambio a través de los recursos. Dado que la biblioteca puede cambiar, y también para mayor comodidad de los lectores, todos los archivos utilizados en el artículo se adjuntan al final del mismo, pero sus versiones se corresponden con el momento de escritura del texto, y pueden distinguirse de las versiones actuales según el en enlace de arriba. Además, las clases de recursos mencionadas usan en su trabajo otra biblioteca más, TypeToBytes, cuya versión también se adjunta al artículo.

No necesitamos profundizar en la construcción interna de estas clases auxiliares. Lo importante es que podemos confiar en la clase preparada RESOURCEDATA como en una "caja negra", y usar su constructor y un par de funciones que nos resulten convenientes. Veremos esto con más detalle un poco más tarde. Ahora vamos a volver al desarrollo del concepto general.

La secuencia de interacción entre las partes de nuestra arquitectura tiene el aspecto que sigue:

  1. Para ejecutar una solicitud asincrónica, el programa de cliente de MQL deberá empaquetar con la ayuda de las clases que estamos desarrollando los parámetros de la solicitud en el recurso local, y luego enviar los eventos personalizados al gestor con el enlace al recurso; el recurso se crea dentro del programa de cliente y no se elimina hasta que se reciban los resultados (cuando deja de ser necesario);
  2. El gestor encuentra en el conjunto un experto-ayudante libre y le redirige el enlace al recurso; en este caso, además, el ejemplar se marca temporalmente con una coma y no puede ser seleccionado para solicitudes posteriores, hasta que no se haya procesado la solicitud actual;
  3. En el experto-ayudante que ha obtenido el evento personalizado tiene lugar el desempaquetado de los parámetros de la solicitud web desde un recurso externo del cliente;
  4. El experto-ayudante llama el WebRequest bloqueante estándar y espera la respuesta (encabezado y/o documento web);
  5. El experto-ayudante empaqueta los resultados de la solicitud en su recurso local y envía el evento personalizado al gestor con el enlace a este recurso;
  6. El gestor reenvía este evento al cliente y marca el ayudante correspondiente como nuevamente disponible;
  7. El cliente recibe un mensaje del gestor y desempaqueta el resultado de la solicitud del recurso externo del ayudante;
  8. El cliente y el ayudante pueden eliminar sus recursos locales.

En los pasos 5 y 6 se puede organizar la transmisión del resultado de forma más efectiva, gracias a que el experto-ayudante envía el resultado directamente a la ventana del cliente, pasando omitiendo el gestor.

Los pasos descritos más adelante se refieren al estadio principal de procesamiento de solicitudes HTTP. Sin embargo, por el momento queda "entre bastidores" el proceso de coordinación de las partes inconexas en una sola estructura. Asimismo, este se apoyará parcialmente en eventos personalizados.

El eslabón central de la arquitectura, el gestor, se debe iniciar manualmente, pero basta con hacerlo una sola vez. Como cualquier experto iniciado, se restaurará automáticamente junto con el gráfico después de reiniciarse el terminal. En el terminal solo se puede tener un gestor de solicitudes web.

El gestor creará el número necesario de ventanas auxiliares (se indicará en los ajustes) e iniciará en ellas varias copias de sí mismo, que "comprenderán" que son ayudantes gracias a un "protocolo" especial (para más detalles, vea el apartado de implementación).

Cualquier ayudante informará sobre su cierre al gestor con la ayuda de un evento especial. Esto es necesario para mantener en el gestor una lista actualizada de ayudantes disponibles. Por analogía, el gestor notificará a los ayudantes sobre su cierre, y los ayudantes en respuesta a esto cesarán el trabajo y cerrarán sus ventanas. Los ayudantes no tienen sentido sin el gestor. El reinicio del gestor, en cualquier caso, creará de nuevo a los ayudantes (por ejemplo, si usted cambia el número de ayudantes en los ajustes).

Las ventanas de los ayudantes, al igual que los propios expertos auxiliares, siempre se deben crear automáticamente desde el gestor, y por eso nuestro programa debe "limpiarlos" tras de sí. Debemos destacar especialmente que no es conveniente iniciar el experto-ayudante manualmente, pues la indicación de unos parámetros de entrada que no se correspondan según su sentido con el papel del gestor será considerada por el programa como un error.

En el momento en que se inicia, el programa MQL de cliente deberá preguntar a la ventana del terminal sobre la presencia del gestor con la ayuda de un enlace masivo, indicando en el parámetro su ChartID. Como respuesta, el gestor (si se localiza) debe retornar el identificador de su ventana al cliente. Después de ello, el cliente y el gestor pueden intercambiar mensajes de trabajo.

Esto es, posiblemente, todo lo referente a la construcción. Vamos a pasar a su implementación.


2. Implementación

Para facilitar el desarrollo, crearemos un archivo de encabezado único, multiweb.mqh, en el que describiremos todas las clases: una parte de ellas será común para el cliente y los "servidores", y otra parte será heredada y específica para cada uno de estos papeles.

2.1. Clases básicas (comienzo)

Vamos a empezar con la clase en la que se guardarán los recursos, los identificadores y las variables de estado de cada uno de los participantes del esquema. Los ejemplares de las clases derivadas de ellos se usarán en el gestor, en los ayudantes y en el cliente. En el cliente y los ayudantes, estos objetos son necesarios en primer lugar para guardar los recursos "enviados por enlace". Aparte de ello, debemos prestar atención a que en el cliente puede haber varios ejemplares creados para ejecutar multitud de solicitudes web de forma paralela, y por eso el análisis de estado de las solicitudes actuales (como mínimo la señal de que el objeto está ocupado o no) debe usarse en los clientes por completo. En el gestor, con la ayuda de los objetos se implementa la identificación y el monitoreo del estado de los ayudantes. Bien, veamos la propia clase básica.

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);
      }
    }
};

Variables:

  • chartID — identificador del gráfico al que está fijado el programa MQL;
  • busy — señal de que el ejemplar dado está ocupado con el procesamiento de una solicitud web;
  • resource — objeto de recurso - repositorio de datos aleatorios; la clase RESOURCEDATA se toma de ResourceData.mqh;
  • prefix — prefijo único para cada papel; el prefijo se usa en el nombre de los recursos; se recomienda hacer una configuración única en cada cliente concreto, como se demostrará a continuación; los expertos-ayudantes usan por defecto el prefijo "WRR_" (abreviatura de Web Request Result);

El método allocate, que se usará en las clases derivadas, crea un objeto de recurso del tipo RESOURCEDATA<uchar> en la variable resource; además, en el nombre del recurso, aparte del prefijo, también existe el identificador del gráfico. La liberación del recurso se puede ejecutar con la ayuda del método release.

Merece la pena destacar el método getFullName, que retorna el nombre completo del recurso, que incluye el nombre y el catálogo de ubicación del programa MQL actual. Precisamente según este nombre completo es posible dirigirse a los recursos de programas de terceros (pero solo de lectura). Por ejemplo, si el experto multiweb.mq5 está ubicado en el catálogo MQL5\Experts e iniciado en un gráfico con el identificador 129912254742671346, el recurso en él recibirá el nombre completo "\Experts\multiweb.ex5::WRR_129912254742671346". Precisamente este tipo de línea vamos a transmitir en calidad de enlace a los recursos, con la ayuda del parámetro de línea sparam de eventos personalizados.

Para buscar el gestor en el futuro, vamos a usar el método estático broadcastEvent, que envía mensajes a todas las ventanas.

Para trabajar con la solicitud y el recurso relacionado con ella en el programa de cliente, vamos a definir la clase ClientWebWorker, derivada de WebWorker (aquí y en lo sucesivo, el código se mostrará con abreviaturas, las versiones completas se encuentran en los archivos adjuntos).

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);
      
      ...
    }
};

Antes de nada, debemos prestar atención a que el método request constituye una implementación factual del paso 1, analizado más arriba. Aquí se realiza el envío de la solicitud web al gestor. La declaración del método sigue al prototipo del WebRequestAsync hipotético. El método estático receiveResult ejecuta la acción inversa al paso 7. Como primer parámetro de entrada, resname obtiene el nombre completo del recurso externo en el que se guardan los resultados de la solicitud; las matrices de bytes initiator, headers y text se deberán rellenar dentro del método con los datos desempaquetados del recurso.

Usted se preguntará qué es initiator. La respuesta es muy sencilla. Puesto que todas nuestras "llamadas" ahora serán asincrónicas (y su orden de ejecución no está garantizado), es imprescindible saber comparar el resultado obtenido con la solicitud enviada anteriormente. Por eso, los expertos-ayudantes empaquetarán en sus recursos de respuesta no solo los datos obtenidos de internet, sino el nombre completo del recurso de cliente original, según el que se ha generado la solicitud. Después de desempaquetar, el nombre entra precisamente en el parámetro initiator y puede usarse para conectar el resultado con la solicitud correspondiente.

El método receiveResult es estático, ya que no usa ninguna varibale de objeto: todos los resultados se retornan al código de llamada a través de los parámetros.

En ambos métodos hay puntos suspensivos donde se necesita directamente el empaquetado y desempaquetado de datos en "y" de sus recursos. Vamos a ocuparnos de esta cuestión en el siguiente apartado.


2.2. Empaquetado de solicitudes y resultados de las solicitudes en los recursos

Como recordamos, se supone que debemos procesar los recursos en el nivel inferior con la clase RESOURCEDATA. Se trata de una clase plantilla, es decir, adopta el parámetro con el tipo de datos que vamos a registrar y leer en el "o" del recurso. Puesto que nuestros datos contienen números y letras, lo lógico es elegir como unidad de guardado el tipo uchar de menor tamaño. De esta forma, el contenedor de datos será un objeto de la clase RESOURCEDATA<uchar>. Al crear el recurso, en su constructor se indica el nombre único (para el programa) name:

RESOURCEDATA<uchar>(const string name)

Precisamente este nombre, complementado por delante con el nombre del programa, podemos transmitirlo en los eventos personalizados, para que otros programas MQL se dirijan al mismo recurso. Preste atención a que todos los demás programas, excepto aquel en cuyo interior se ha creado el recurso, tienen acceso solo a la lectura.

El registro de datos en el recurso se realiza con la ayuda del operador de asignación sobrecargado:

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

donde array es la matriz que debemos preparar.

La lectura de datos desde el recurso se realiza con la ayuda de la función:

int Get(uchar &array[]) const

Aquí array es el parámetro de salida en el que se ubicará el contenido de la matriz original.

Ahora vamos a analizar el aspecto aplicado del uso de recursos para la transmisión de datos sobre solicitudes HTTP y sus resultados. Creamos una clase-capa entre los recursos y nuestro código básico: ResourceMediator. Esta empaquetará en el lado del cliente los parámetros method, url, headers, timeout, data en una matriz de bytes array y después los registrará en el recurso; en el lado del servidor, desempaquetará los parámetros del recurso. De una forma análoga, esta clase empaquetará en el lado del servidor los parámetros result y result_headers en una matriz de datos array y los registrará en el recurso para después leerlos como una matriz y desempaquetarlos en lado del cliente.

El constructor ResourceMediator recibe el puntero al recurso RESOURCEDATA, que después se procesará dentro de los métodos. Además, ResourceMediator contiene estructuras auxiliares para el almacenamiento de meta-información sobre los datos. En verdad, al empaquetar y desempaquetar los recursos, aparte de los propios datos, necesitaremos un encabezado que contenga las dimensiones de todos los campos.

Por ejemplo, si simplemente usamos la función StringToCharArray para convertir un URL en una matriz de bytes, al ejecutar la operación inversa con la ayuda de CharArrayToString, será necesario indicar la longitud de la matriz. En el caso contrario — si no la indicamos — desde la matriz se leerán no solo los bytes del URL, sino el campo con encabezado que viene detrás de él. Recordemos que colocamos todos los datos en una matriz única antes de registrarlos en el recurso. La meta-información sobre la longitud de los campos deberá formarse en una secuencia de bytes, para lo cual se usan uniones (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;
    }
    
    ...

Lo primero que hace el método packRequest es registrar las dimensiones de todos los campos en la estructura metadata. A continuación, el contenido de esta estructura, pero en forma de matriz de bytes, se copia al inicio de la matriz data, que como consecuencia de ello será ubicada en el recurso. El tamaño de la matriz data se reserva partiendo de la longitud total de todos los campos y el tamaño de la propia estructura con metadatos. Los parámetros del tipo string se transforman en matrices con la ayuda de StringToCharArray y se copian en la matriz resultante según el desplazamiento correspondiente, que se mantiene todo el tiempo en el estado actual en la variable cursor. El parámetro timeout se transforma en una matriz de símbolos con la ayuda de la unión int2chars. El parámetro body se copia a la matriz tal cual, puesto que ya supone una matriz del tipo necesario. Finalmente, el propio traslado del contenido de la matriz general al recurso se realiza en la línea (recordemos que el operador = sobrecarga en la clase RESOURCEDATA):

      resource = data;

La operación inversa de extracción de los parámetros de la solicitud desde el recurso se ejecuta en el 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);
      }
    }
    
    ...

Aquí el trabajo principal lo realiza la línea con la llamada resource.Get(array). A continuación, desde la matriz array se leen por etapas los bytes de metadatos y, sobre su base, todos los campos posteriores.

De una forma análoga se empaquetan y desempaquetan los resultados de ejecución de las solicitudes con la ayuda de los métodos packResponse y unpackResponse (el código completo se encuentra en los anexos).

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

Ahora podemos retornar al código fuente de ClientWebWorker y finalizar los métodos request y 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);
    }
};

Han resultado bastante sencillos gracias al uso de la clase ResourceMediator, que ha asumido todo el trabajo rutinario.

Solo quedan las incógnitas: ¿cuándo y dónde llamar los métodos de los WebWorker y cómo recibir los valores de algunos parámetros auxiliares, tales como managerChartID en el método request? Adelantándonos un poco, responderemos que es lógico asignar el control de todos los objetos de las clases WebWorker a clases de un nivel superior, que den soporte a las listas actuales de objetos e intercambien mensajes entre los programas "en nombre" de los objetos, incluyendo los mensajes de búsqueda del gestor. Pero antes de pasar a este nuevo nivel, debemos finalizar una preparación análoga en la "parte del servidor".


2.3. Clases básicas (continuación)

La clase ClientWebWorker se ocupa de las solicitudes asincrónicas exactemente de la misma forma en el lado del cliente; en el "lado del servidor" — en el gestor — para este cometido declararemos nuestra propia clase derivada de WebWorker.

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);
    }
};

El método transfer delega el procesamiento de la solicitud en un ejemplar concreto del experto-ayudante, de acuerdo con el paso 2 en la secuencia general de interacción. El parámetro resname supone el nombre del recurso recibido del cliente, mientras que clientChartID es el identificador de la ventana de cliente. Todos estos parámetros los obtenemos de los eventos personalizados. Los eventos personalizados, en concreto MSG_WEB, se describirán más abajo.

El método receive crea el recurso local en el objeto actual WebWorker (llamada de allocate) y registra allí el nombre del recurso-iniciador original de la solicitud (source), así como los datos recibidos de internet (result) y los encabezados HTTP (result_headers), con la ayuda del objeto mediator de la clase ResourceMediator. Esta es una parte del paso 5 de la secuencia general.

Bien, ya hemos definido las clases de los WebWorker tanto para el cliente, como para el "servidor". En ambos casos, estos objetos se crearán, seguramente, en grandes cantidades. Por ejemplo, un cliente puede descargar varios documentos de golpe, mientras, en el lado del gestor, será deseable distribuir inicialmente un número suficiente de ayudantes, puesto que las solicitudes pueden llegar simultáneamente de muchos clientes. Para organizar la matriz de objetos, definimos la clase básica especial WebWorkersPool. La haremos clase plantilla, puesto que el tipo de objetos guardados se distinguirá en el cliente y en el "servidor", respectivamente, ClientWebWorker y ServerWebWorker.

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];
    }
    
};

La esencia de los métodos es sencilla. El constructor y el destructor seleccionan y liberan la matriz de procesadores del tamaño designado. El grupo de métodos findWorker y getIdleWorker realiza la búsqueda de objetos en la matriz según distintos criterios. El operador operator<< permite añadir objetos dinámicamente, y el método revoke, eliminarlos dinámicamente.

El conjunto de manejadores en el lado del cliente debe poseer ciertas características específicas (concretamente, en lo referente al procesamiento de eventos), por eso, vamos a ampliar la clase básica con la ayuda de ClientWebWorkersPool derivada.

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;
    }
};

Variables:

  • managerChartID — identificador de la ventana donde se encuentra el gestor en funcionamiento;
  • managerPoolSize — número inicial de la matriz de objetos-manejadores;
  • name — prefijo general para los recursos en todos los objetos del pool;


2.4. Intercambio de mensajes

En el constructor ClientWebWorkersPool, vemos la llamada de WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()), que envía el evento MSG_DISCOVER a todas las ventanas, transmitiendo el identificador de la ventana actual en el parámetro del evento. MSG_DISCOVER es un cierto valor reservado: este, junto con otros tipos de mensajes que intercambiarán nuestros programas, deberá ser determinado al inicio de este archivo de encabezado.

#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)

En los comentarios se marca la dirección en la que se envía este u otro mensaje.

El macros TO_MSG se ha pensado para convertir los identificadores enumerados en códigos reales de eventos con respecto a la magnitud básica aleatoria elegida por el usuario. Esta la obtendremos a través del parámetro de entrada MessageBroadcast.

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

Este enfoque permite, modificando el valor básico, desplazar todos los eventos a cualquier intervalo libre. Recordemos que los eventos propios también pueden ser usados en el terminal por otros programas, precisamente por ello es importante evitar colisiones.

El parámetro de entrada MessageBroadcast aparecerá en todos nuestros programas MQL en los que esté incluido el archivo multiweb.mqh, es decir, tanto en los clientes, como en el gestor. Al iniciar el gestor y los clientes, es conveniente indicar el mismo valor MessageBroadcast.

Volvamos a la clase ClientWebWorkersPool. El método onChartEvent ocupa un lugar especial. Se llamará desde el procesador estándar del evento OnChartEvent. El tipo de evento se transmite en el parámetro id. Puesto que obtenemos del sistema los códigos teniendo en cuenta el valor básico elegido, para su transmisión de retorno al intervalo MSG_*** usaremos el macros MSG "inverso":

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

CHARTEVENT_CUSTOM, aquí se trata del inicio del intervalo para todos los eventos personalizados en el terminal.

Como podemos ver, el método onChartEvent en ClientWebWorkersPool procesa algunos de los mensajes mencionados más arriba. Bien, en respuesta al envío masivo MSG_DISCOVER, el gestor debe responder con el mensaje MSG_HELLO. En este caso, en el parámetro lparam se transmite el identificador de la ventana del gestor, y en el parámetro dparam para la guía, el número de ayudantes disponibles. Cuando un gestor es detectado, el pool rellena la matriz workers, vacía hasta ese momento, con objetos reales del tipo necesario. En este caso, además, se transmite al constructor de objetos el indicador de la ventana actual, así como el nombre único para el recurso en cada objeto; dicho nombre consta del prefijo general name y el número ordinal en la matriz.

Después de que el campo managerChartID haya recibido el valor, aparece la posibilidad de enviar solicitudes al gestor. Para ello, en la clase ClientWebWorker se ha creado el método request, cuyo uso se muestra en el método WebRequestAsync del pool. El método WebRequestAsync encuentra por primera vez un objeto-procesador con la ayuda de getIdleWorker, y a continuación llama para él worker.request(method, url, headers, timeout, data, managerChartID). Dentro del método request teníamos un comentario relativo a la elección del código especial de los mensajes para el envío del evento. Ahora, después de analizar el subsistema de eventos, podemos formar la versión definitiva del 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 — mensaje sobre la ejecución de la solicitud web. Tras recibirlo, el gestor deberá encontrar un experto-ayudante libre y transmitir al mismo el nombre del recurso de cliente (sparam) con las características de la solicitud, así como el identificador de la ventana de cliente chartID (lparam).

El ayudante ejecuta inmediatamente la solicitud y retorna los resultados al cliente con la ayuda del evento MSG_DONE (en caso de éxito) o el código de error con la ayuda de MSG_ERROR (si hubiera problemas). El código del resultado (o el error) se transmite a dparam, mientras que el propio resultado se empaqueta en el recurso que se encuentra en el experto-ayudante bajo el nombre transmitido a sparam. En la rama MSG_DONE, podemos ver cómo del recurso se extraen los datos con la ayuda de la llamada de la función vista anteriormente ClientWebWorker::receiveResult(sparam, initiator, headers, text). A continuación, según el nombre del recurso del iniciador de la solicitud, se busca el objeto-procesador de cliente (findWorker) y en el objeto encontrado se llama un par de métodos:

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

El método release ya lo conocemos, se encarga de liberar un recurso innecesario que ya ha trabajado lo requerido. El método onResult, en cambio, es algo nuevo. En realidad, si echamos un vistazo al código fuente completo, en la clase ClientWebWorker existen 2 funciones virtuales sin implementación: onResult y onError. Esto hace de la clase una clase abstracta. El código de cliente debe describir su clase derivada de ClientWebWorker y ofrecer la implementación de ambas. Por el propio nombre de los métodos podemos comprender que onResult se llama en caso de que hayamos recibido los resultados con éxito, y onError, en caso de error. De esta forma, se posibilita una respuesta mutua entre las clases de trabajo de las solicitudes asincrónicas y el código del programa de cliente, que usa aquellas. En otras palabras, el programa de cliente no necesita saber nada sobre los mensajes que usa el núcleo en su interior: todas las interacciones del código de cliente con la API desarrollada se llevan a cabo con los métodos estándar de la POO de MQL5.

Echemos un vistazo al código fuente del cliente, multiwebclient.mq5.


2.5. Experto de cliente

Nuestro experto de prueba enviará, basándose en los datos introducidos por el usuario, varias solicitudes a través de multiweb API. Para ello, es necesario activar el archivo de encabezado y añadir los 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 los parámetros tienen la misión, a fin de cuentas, de configurar las solicitudes HTTP realizadas. En el campo URL, podemos enumerar varias direcciones usando comas, para valorar el carácter paralelo y la velocidad de ejecución de las solicitudes. El parámetro URL se divide en direcciones con la ayuda de la función StringSplit в OnInit, aproximadamente de la forma que sigue:

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

Además, en OnInit es necesario crear un conjunto de objetos-manejadores de solicitudes (ClientWebWorkersPool), pero para hacer esto, debemos ante todo describir nuestra clase, que es heredera 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);
    }
};

En la práctica, esta clase no hace nada más que mostrar en el log la información sobre el estado y los datos recibidos. Ahora podemos crear un pool de estos objetos en OnInit.

ClientWebWorkersPool<MyClientWebWorker> *pool = NULL;

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

Como puede ver, el conjunto se parametriza con la clase MyClientWebWorker, lo que da la posibilidad de crear nuestros objetos a partir del código de la biblioteca. El tamaño de la matriz se elige como igual al número de direcciones introducidas. Esto resulta óptimo desde un punto de vista demostrativo: un número menor indicaría que existe una cola de procesamiento y se da una discretización de ideas paralela a la ejecución, y un número mayor, que se están gastando recursos de más. En los proyectos reales, el tamaño del pool, como es lógico, no debe ser igual al número de tareas, pero esto demandaría una vinculación algorítmica adicional.

El prefijo para los recursos se establece como la combinación del símbolo de trabajo y el periodo del gráfico.

El último detalle relacionado con la inicialización es la búsqueda de la ventana del gestor. Como podrá usted recordar, de esto se encarga el propio pool, la clase ClientWebWorkersPool. Por parte del código de cliente, solo se requiere asegurarse de que el gestor haya encontrado. Para estos objetivos, nos daremos un tiempo razonable, transcurrido el cual, el mensaje sobre la búsqueda del gestor y el "saludo" de respuesta deberán alcanzar su objetivo con toda garantía. Pongamos que sean 5 segundos. Vamos a crear un temporizador para este tiempo:

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

En el manejador del temporizador comprobamos el gestor y mostramos una alerta, si no se ha establecido conexión.

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");
  }
}

En el manejador OnDeinit debemos recordar eliminar el objeto del pool.

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

Para que el pool procese todos los mensajes auxiliares "por nosotros", incluyendo, en primer lugar, la búsqueda del gestor, será necesario implicar el manejador estándar de eventos del 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 los eventos, excepto CHARTEVENT_CLICK, son enviados al pool, donde, usando como base el análisis de los códigos de los eventos aplicados, se ejecutan las acciones correspondientes (más abajo se muestra un fragmento de onChartEvent).

El evento CHARTEVENT_CLICK es interactivo y se usa propiamente para iniciar los procesos de descarga. Un caso sencillo puede ser, por ejemplo:

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);
      }
    }
    ...

El código completo del ejemplo es más voluminoso, puesto que se le ha añadido la lógica de cálculo del tiempo de ejecución y la comparación del mismo con la llamada posterior del WebRequest estándar para el mismo conjunto de direcciones.


2.6. Experto-gestor, él mismo es el experto-ayudante

De toda la arquitectura nos queda analizar solo la parte del "servidor". Gracias a que los mecanismos básicos ya han sido implementados en el archivo de encabezado, el código del gestor y los ayudantes ya no es tan enorme como podría imaginarse.

Antes de nada, debemos recordar que solo tenemos un experto que funciona o bien como gestor o bien como ayudante: el archivo multiweb.mq5. Como sucede en el caso con el cliente, conectamos el archivo de encabezado y declaramos los parámetros de entrada:

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

#include <multiweb.mqh>

WebRequestPoolSize es el número de ventanas auxiliares que el gestor debe crear e iniciar en sus ayudantes.

ManagerChartID es el identificador de la ventana del gestor. Este parámetro tiene sentido solo como ayudante, y será rellenado por el propio gestor al iniciar automáticamente los ayudantes desde el código fuente. El rellenado manual de ManagerChartID al iniciar el gestor es tratado como un error.

El algoritmo se construye en torno a dos variables globales:

bool manager;
WebWorkersPool<ServerWebWorker> pool;

La bandera lógica manager contiene el signo que indica con qué papel actúa el ejemplar actual del experto. La variable pool es la matriz de objetos-manejadores de las tareas de entrada. WebWorkersPool es tipificado por la clase ServerWebWorker, que ya hemos analizado más arriba. La matriz no se inicializa por anticipado, porque su rellenado depende del papel.

El papel del gestor se le otorga al primer ejemplar iniciado, lo cual se determina en 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;
}

El experto comprueba la presencia de una variable global especial del terminal. Si no está, el experto se designa a sí mismo como gestor y crea esta variable global. Si la variable ya existe, significa que el gestor también existe, y por eso este ejemplar se convierte en ayudante. Preste atención a que la variable global es temporal, es decir, no se guardará al reiniciarse el terminal. Si el gestor se ha dejado en algún gráfico, creará la variable de nuevo.

A continuación, se instala un temporizador de 1 segundo, puesto que la inicialización de los gráficos auxiliares normalmente necesita un par de segundos, y hacer esto desde OnInit no es muy bonito. En el manejador del evento del temporizador, rellenamos el 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_");
  }
}

En el caso del papel del ayudante, simplemente se añade a la matriz el objeto-manejador ServerWebWorker. El caso del gestor es más complejo, por eso se muestra en la función instantiateWorkers. Vamos a echarle un vistazo por separado.

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 función usa la biblioteca de terceros Expert de nuestro viejo conocido de la comunidad MQL5, fxsaber, por eso, al comienzo del código fuente, se ha añadido el archivo de encabezado correspondiente.

#include <fxsaber\Expert.mqh>

La biblioteca Expert permite formar dinámicamente plantillas tpl con los parámetros de los expertos indicados y aplicarlas a los gráficos indicados, lo cual provoca de hecho el inicio de los expertos. En nuestro caso, los parámetros de todos los expertos-ayudantes coinciden, por eso su lista se forma una sola vez antes de la creación del número de ventanas establecido.

En el parámetro 0 se indica la ruta al archivo ejecutable del experto, es decir, a sí mismo. El parámetro 1 es WebRequestPoolSize. En cada ayudante es igual a 1. Recordemos que en el ayudante, el objeto-manejador es necesario para guardar el recurso con los resultados de la solicitud HTTP. Cada ayudante procesa la solicitud con WebRequest bloqueantes, es decir, implica un máximo de un objeto-procesador. El parámetro 2 es el identificador de la ventana del gestor ManagerChartID. El parámetro 3 es el valor básico para los códigos de los mensajes (el parámetro MessageBroadcast se toma de multiweb.mqh).

A continuación, se crean en el ciclo gráficos vacíos con la ayuda de ChartOpen y en ellos se inician los expertos-ayudantes con la ayuda de EXPERT::Run(chart, Params). Para cada nueva ventana se crea un objeto-manejador ServerWebWorker(chart) y se añade al pool. Conviene notar que en el gestor, los objetos-manejadores no son más que enlaces a los identificadores de ventanas de los ayudantes y sus estados, puesto que en el propio gestor no se ejecutan las solicitudes HTTP y no se crean los recursos para ellas.

El procesamiento de las tareas entrantes se ejecuta sobre la base de los eventos personalizados en 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();
    }
  }
}

Ante todo, como respuesta al mensaje MSG_DISCOVER, recibido del cliente con el identificador lparam, el gestor le responde con el mensaje MSG_HELLO, en el que indica el identificador de su ventana.

Al llegar MSG_WEB, en lparam debe encontrarse el identificador de la ventana de cliente que ha enviado la solicitud, y en sparam, el nombre del recurso con los parámetros de la solicitud empaquetados. Trabajando en el papel de gestor, el código intenta transmitir la tarea con estos parámetros a un ayudante libre, llamando la función transfer (se analizará más abajo), y con ello transfiriendo el objeto elegido al estado ocupado ("busy"). Si no hay ayudantes libres, al cliente se le envía el evento MSG_ERROR con el código ERROR_NO_IDLE_WORKER. En el papel de ayudante se debe ejecuctar de inmediato la solicitud HTTP, lo cual se realiza en la función startWebRequest.

El evento MSG_DONE llega al gestor desde el ayudante, cuando aquel ha descargado el documento solicitado. Una vez encontrado el objeto correspondiente según el identificador del ayudante en lparam, el gestor le quita el estado "busy", llamando el método release. Recordemos que el ayudante le envía directamente el resultado de su trabajo al cliente.

En el código fuente completo podemos detectar otro evento MSG_DEINIT, que está estrechamente relacionado con el procesamiento de OnDeinit. La esencia reside en que los asesores comprueban la eliminación del gestor y como respuesta a ello se descargan a sí mismos y cierran su ventana, mientras que el gestor comprueba la eliminación del ayudante y lo tacha de su pool. Deberemos desentrañar este mecanismo por nuestra propia cuenta.

La función transfer busca un objeto libre y llama su método transfer (que se ha analizado más arriba).

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

La función startWebRequest se determina de la siguiente 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 el mediador de recursos ResourceMediator, la función desempaqueta los parámetros de la solicitud y llama la función estándar MQL WebRequest. Si la última se ha ejecutado sin errores MQL, los resultados se envían al cliente. Para ello, se empaquetan en el recurso local con la ayuda de la llamada del método receive (que hemos analizado anteriormente) y su nombre se transmite con el mensaje MSG_DONE en el parámetro sparam de la función EventChartCustom. Preste atención a que los errores HTTP (por ejemplo, la página 404 incorrecta, o el error de servidor web 501) entran aquí, el cliente recibe un código HTTP en el parámetro dparam y los recursos HTTP de respuesta en el recurso, lo que permite analizar la situación.

Si la llamada WebRequest ha finalizado con un error MQL, al cliente se le envía el mensaje MSG_ERROR con el código ERROR_MQL_WEB_REQUEST, y el resultado GetLastError se ubica en dparam. Puesto que el recurso local en este caso no se rellena, al parámetro sparam se le transmite directamente el nombre del recurso original, para que en el lado del cliente se pueda seguir identificando un ejemplar concreto del objeto-manejador con el recurso.

Diagrama de clases de la biblioteca multiweb para la llamada asincrónica y paralela de WebRequest

Fig.4. Diagrama de clases de la biblioteca multiweb para la llamada asincrónica y paralela de WebRequest


3. Demostración de las posibilidades y simulación

El procedimiento de simulación del complejo programático implementado puede ser, por ejemplo, así.

Debemos abrir de forma preliminar los ajustes del terminal e introducir en la pestaña Expertos, en la lista de URL permitidos para WebRequest, todos los servidores a los que se planea recurrir.

A continuación, iniciamos el experto multiweb e indicamos en los parámetros de entrada 3 ayudantes. Como resultado, se abrirán 3 nuevas ventanas, en las que estará iniciado el mismo experto multiweb, pero en otro papel. El papel del experto se muestra en el comentario en la esquina superior izquierda de la ventana.

Ahora iniciamos en otro gráfico el experto de cliente multiwebclient y clicamos una vez con el ratón en el gráfico. Con la instalación por defecto, inicia 3 solicitudes web paralelas y escribe el diagnóstico en el log, incluyendo el tamaño de los datos obtenidos y la hora de funcionamiento. Si el parámetro especial TestSyncRequests se ha dejado con el valor true, se ejecutan de forma adicional a las solicitudes web paralelas las solicitudes consecutivas de aquellas mismas páginas con la ayuda del WebRequest estándar. Esto se ha hecho para comparar la velocidad de ejecución de las dos variantes. Como norma, el procesamiento paralelo es varias veces más rápido que el consecutivo: de sqrt(N) a N, donde N es el número de ayudantes disponibles.

Abajo mostramos un ejemplo del log.

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

Preste atención a que el tiempo total de ejecución de todas las solicitudes es igual al tiempo de ejecución de la solicitud más lenta.

Resulta obvio que si establecemos para el gestor un número de ayudantes igual a 1, las solicitudes se procesarán de forma secuencial.


Conclusión

En este artículo se ha analizado un conjunto de clases y expertos preparados para ejecutar solicitudes HTTP en el modo no bloqueante. Esto permite obtener los datos de Internet en varios flujos paralelos, así como aumentar el rendimiento de los expertos que deben ejecutar, aparte de solicitudes HTTP, cálculos analíticos en tiempo real. Además, esta biblioteca se puede utilizar no solo en los expertos, sino también en los indicadores donde el WebRequest estándar está prohibido. Para implementar toda la arquitectura, hemos tenido que usar el amplio espectro de posibilidades de MQL: la transmisión de eventos personalizados, la creación de recursos, la apertura dinámica de ventanas y el inicio de los expertos en ellas.

En el momento en que se ha escrito este artículo, la creación de ventanas auxiliares para el inicio de expertos-ayudantes es la única variante para las solicitudes HTTP paralelas, sin embargo, en los planes de la empresa MetaQuotes entra el desarrollo de programas MQL especiales de fondo: servicios para los cuales ya se ha reservado la carpeta MQL5/Services. Cuando esta tecnología aparezca en el terminal, lo más seguro es que sea posible mejorar esta biblioteca sustituyendo las ventanas auxiliares por servicios.

Lista de archivos adjuntos:

  • MQL5/Include/multiweb.mqh — biblioteca
  • MQL5/Experts/multiweb.mq5 — experto-gestor y experto-ayudante 
  • MQL5/Experts/multiwebclient.mq5 — experto-cliente de demostración
  • MQL5/Include/fxsaber/Resource.mqh — clase auxiliar para trabajar con los recursos
  • MQL5/Include/fxsaber/ResourceData.mqh — clase auxiliar para trabajar con los recursos
  • MQL5/Include/fxsaber/Expert.mqh — clase auxiliar para iniciar los expertos
  • MQL5/Include/TypeToBytes.mqh — biblioteca de conversión de datos


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/5337

Archivos adjuntos |
MQL5.zip (17.48 KB)
Imanol Salazar Garcia
Imanol Salazar Garcia | 6 feb. 2019 en 02:27
Awesome article. Openning doors for a lot of possibilities!. Thanks you.
Aplicación de OpenCL para simular patrones de velas Aplicación de OpenCL para simular patrones de velas
En este artículo analizaremos el algoritmo de implementación de un simulador de patrones de velas en el lenguaje OpenCL en el modo "OHLC en M1". Asimismo, compararemos su rapidez con el simulador de estrategias incorporado en el modo de optimización rápida y lenta.
Las 100 mejores pasadas de optimización (Parte 1). Creando un analizador de optimizaciones Las 100 mejores pasadas de optimización (Parte 1). Creando un analizador de optimizaciones
En este artículo hablaremos sobre cómo crear una aplicación para seleccionar las mejores pasadas de optimización según varias opciones posibles. Esta aplicación sabe filtrar y clasificar los resultados de optimización según multitud de coeficientes. Las pasadas de optimización se registran en una base de datos, por eso usted siempre podrá seleccionar nuevos parámetros de trabajo sin tener que reoptimizar. Además, esto permite ver todas las pasadas de optimización en un único gráfico, calcular los coeficientes VaR paramétricos y construir el gráfico de distribución normal de las pasadas y resultados de comercio de la variante de combinación de coeficientes seleccionada. Asimismo, se construyen los gráficos de algunos de los coeficientes en una dinámica, comenzando desde el momento de inicio de la optimización (o desde una fecha seleccionada hasta otra fecha seleccionada).
Reversión: creando un punto de entrada y escribiendo un algoritmo de comercio manual Reversión: creando un punto de entrada y escribiendo un algoritmo de comercio manual
Este es el último artículo de la serie dedicada a la estrategia comercial de la reversión. En él intentaremos solucionar un problema que ha provocado inestabilidad en los resultados de la simulación en los anteriores artículos. Asimismo, escribiremos y simularemos nuestro propio algoritmo para el comercio manual en cualquier mercado con la ayuda de la reversión.
Reversión: disminuyendo la reducción máxima y simulando otros mercados Reversión: disminuyendo la reducción máxima y simulando otros mercados
En este artículo continuaremos analizando el tema de la reversión. Intentaremos disminuir la reducción máxima del balance hasta un nivel aceptable con los instrumentos analizados anteriormente. También vamos a comprobar si se reduce el beneficio obtenido. Asimismo, comprobaremos cómo funciona la reversión en otros mercados, tales como los mercados de valores, materias primas, índices y ETF, agrario. ¡Atención, el artículo contiene muchas imágenes!