Usar WinInet.dll para el intercambio de datos entre terminales por internet

--- | 7 mayo, 2014

MetaTrader 5 brinda a los usuarios unas oportunidades únicas, mediante un abanico de elementos en el nuevo interfaz de usuario. Por eso, se ahora se pueden utilizar al máximo las funciones que no estaban disponibles antes.

En este trabajo aprenderemos a:

En el CodeBase de MQL5 hay un ejemplo de un script que funciona con la librería wininet.dll y muestra el ejemplo de petición de una página del servidor. Pero hoy vamos a ir aún más lejos, haciendo que el servidor no solo nos proporcione la página, sino también envía y almacena estos datos para su envío posterior a otros terminales que los soliciten.

Nota: para aquellos que no tengan acceso a un servidor configurado con PHP, sugerimos descargar el kit Denwer, y usarlo como una plataforma de trabajo. Además, recomendamos utilizar el servidor Apache y PHP para las pruebas locales.

Para enviar cualquier petición al servidor, necesitaremos las 7 principales funciones de la librería.

InternetAttemptConnect  Intenta encontrar y establecer una conexión a internet
InternetOpen
Inicializa la estructura para el funcionamiento de las funciones de la librería WinInet. Hay que activar esta función antes de utilizar cualquier otra función de la librería.
InternetConnect Abre los recursos indicados por la dirección HTTP URL o FTP. Devuelve el descriptor a una conexión abierta
HttpOpenRequest Crea el descriptor de las peticiones HTTP para establecer la conexión
HttpSendRequest Envía una petición mediante el descriptor creado
InternetReadFile Lee los datos recibidos del servidor después de que se haya procesado la petición
InternetCloseHandle Libera el descriptor enviado

 
El sistema de ayuda de MSDN contiene una descripción detallada de las funciones y de sus parámetros.

La declaración de las funciones sigue siendo igual que en MQL4, excepto el uso de las llamadas Unicode y las líneas de transferencias mediante el enlace.

#import "wininet.dll"
int InternetAttemptConnect(int x);
int InternetOpenW(string &sAgent,int lAccessType,string &sProxyName,string &sProxyBypass,int lFlags);
int InternetConnectW(int hInternet,string &szServerName,int nServerPort,string &lpszUsername,string &lpszPassword,int dwService,int dwFlags,int dwContext);
int HttpOpenRequestW(int hConnect,string &Verb,string &ObjectName,string &Version,string &Referer,string &AcceptTypes,uint dwFlags,int dwContext);
int HttpSendRequestW(int hRequest,string &lpszHeaders,int dwHeadersLength,uchar &lpOptional[],int dwOptionalLength);
int HttpQueryInfoW(int hRequest,int dwInfoLevel,int &lpvBuffer[],int &lpdwBufferLength,int &lpdwIndex);
int InternetReadFile(int hFile,uchar &sBuffer[],int lNumBytesToRead,int &lNumberOfBytesRead);
int InternetCloseHandle(int hInet);
#import

//To make it clear, we will use the constant names from wininet.h.
#define OPEN_TYPE_PRECONFIG     0           // use the configuration by default
#define FLAG_KEEP_CONNECTION    0x00400000  // do not terminate the connection
#define FLAG_PRAGMA_NOCACHE     0x00000100  // no cashing of the page
#define FLAG_RELOAD             0x80000000  // receive the page from the server when accessing it
#define SERVICE_HTTP            3           // the required protocol

La misma sección de MSDN contiene una descripción detallada de los flags (marcadores). Si desea ver la declaración de otras constantes y funciones, puede descargar el archivo original wininet.h, que se encuentra en los archivos adjuntos al artículo.

1. Guías para crear y eliminar una sesión de internet

Lo primero que tenemos que hacer es crear una sesión y abrir una conexión al host. Es preferible crear una sesión una sola vez durante la inicialización del programa (por ejemplo, en una función OnInit). También se puede hacer al principio del lanzamiento del Asesor Experto, pero es muy importante asegurarse de su correcta creación justo antes del cierre de la sesión. Y no debe ser llamada repetidamente e innecesariamente con cada nueva iteración de la implementación OnStart u OnTimer. Es importante evitar llamadas frecuentes y crear las estructuras necesarias para cada llamada.

Por tanto, usaremos solamente una instancia de clase genérica para describir y conectar los descriptores.

   string            Host;       // host name
   int               Port;       // port
   int               Session;    // session descriptor
   int               Connect;    // connection descriptor

bool MqlNet::Open(string aHost,int aPort)
  {
   if(aHost=="")
     {
      Print("-Host is not specified");
      return(false);
     }
   // checking the DLL resolution in the terminal  
   if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED))
     {
      Print("-DLL is not allowed");
      return(false);
     }
   // if the session was identifies, then we close
   if(Session>0 || Connect>0) Close();
   // record of attempting to open into the journal
   Print("+Open Inet...");
   // if we were not able to check for the presence of an Internet connection, then we exit
   if(InternetAttemptConnect(0)!=0)
     {
      Print("-Err AttemptConnect");
      return(false);
     }
   string UserAgent="Mozilla"; string nill="";
   // open a session
   Session=InternetOpenW(UserAgent,OPEN_TYPE_PRECONFIG,nill,nill,0);
   // if we were not able to open a session, then exit
   if(Session<=0)
     {
      Print("-Err create Session");
      Close();
      return(false);
     }
   Connect=InternetConnectW(Session,aHost,aPort,nill,nill,SERVICE_HTTP,0,0);
   if(Connect<=0)
     {
      Print("-Err create Connect");
      Close();
      return(false);
     }
   Host=aHost; Port=aPort;
   // otherwise all attempts were successful
   return(true);
  }

Después de la inicialización, se pueden usar los descriptores Session y Connect en todas las funciones posteriores. Una vez finalizado todo el trabajo y desinstalados los programas MQL, hay que borrarlos. Esto se hace mediante la función InternetCloseHandle.

void MqlNet::CloseInet()
  {
   Print("-Close Inet...");
   if(Session>0) InternetCloseHandle(Session); Session=-1;
   if(Connect>0) InternetCloseHandle(Connect); Connect=-1;
  }

¡Atención! Al trabajar con las funciones de Internet, es necesario liberar todos los descriptores derivados de las mismas, mediante InternetCloseHandle.

2. El envío de una petición al servidor y la recepción de la página

Para enviar una petición y recibir una página en respuesta a la misma, necesitaremos las tres funciones restantes HttpOpenRequest, HttpSendRequest y InternetReadFile. El proceso de la recepción de la página en respuesta a la petición es básicamente el sencillo proceso de guardar sus contenidos en un archivo local.

Para trabajar más cómodamente con las peticiones y contenidos, crearemos dos funciones genéricas.

El envío de una petición:

bool MqlNet::Request(string Verb,string Object,string &Out,bool toFile=false,string addData="",bool fromFile=false)
  {
   if(toFile && Out=="")
     {
      Print("-File is not specified ");
      return(false);
     }
   uchar data[];
   int hRequest,hSend,h;
   string Vers="HTTP/1.1";
   string nill="";
   if(fromFile)
     {
      if(FileToArray(addData,data)<0)
        {
         Print("-Err reading file "+addData);

         return(false);
        }
     } // read file in the array
   else StringToCharArray(addData,data);

   if(Session<=0 || Connect<=0)
     {
      Close();
      if(!Open(Host,Port))
        {
         Print("-Err Connect");
         Close();
         return(false);
        }
     }
   // create a request descriptor
   hRequest=HttpOpenRequestW(Connect,Verb,Object,Vers,nill,nill,FLAG_KEEP_CONNECTION|FLAG_RELOAD|FLAG_PRAGMA_NOCACHE,0);
   if(hRequest<=0)
     {
      Print("-Err OpenRequest");
      InternetCloseHandle(Connect);
      return(false);
     }
   // send request
   // headline for request
   string head="Content-Type: application/x-www-form-urlencoded";
   // sent file
   hSend=HttpSendRequestW(hRequest,head,StringLen(head),data,ArraySize(data)-1);
   if(hSend<=0)
     {
      Print("-Err SendRequest");
      InternetCloseHandle(hRequest);
      Close();
     }
   // read the page 
   ReadPage(hRequest,Out,toFile);
   // close all handles
   InternetCloseHandle(hRequest); 
   InternetCloseHandle(hSend);
   return(true);
  }

Parámetros de la función MqlNet:: Request:

Lectura de los contenidos del descriptor recibido

void MqlNet::ReadPage(int hRequest,string &Out,bool toFile)
  {
   // read the page 
   uchar ch[100];
   string toStr="";
   int dwBytes,h;
   while(InternetReadFile(hRequest,ch,100,dwBytes))
     {
      if(dwBytes<=0) break;
      toStr=toStr+CharArrayToString(ch,0,dwBytes);
     }
   if(toFile)
     {
      h=FileOpen(Out,FILE_BIN|FILE_WRITE);
      FileWriteString(h,toStr);
      FileClose(h);
     }
   else Out=toStr;
  }

Parámetros de la función MqlNet:: ReadPage:

Y juntándolas todas en una, obtendremos una clase de la librería MqlNet para trabajar con internet.

class MqlNet
  {
   string            Host;     // host name
   int               Port;     // port
   int               Session; // session descriptor
   int               Connect; // connection descriptor
public:
                     MqlNet(); // class constructor
                    ~MqlNet(); // destructor
   bool              Open(string aHost,int aPort); // create a session and open a connection
   void              Close(); // close session and connection
   bool              Request(string Verb,string Request,string &Out,bool toFile=false,string addData="",bool fromFile=false); // send request
   bool              OpenURL(string URL,string &Out,bool toFile); // somply read the page into the file or the variable
   void              ReadPage(int hRequest,string &Out,bool toFile); // read the page
   int               FileToArray(string FileName,uchar &data[]); // copy the file into the array for sending
  };

Estas son básicamente todas las funciones requeridas para satisfacer las diversas necesidades de trabajar con internet. Considere los ejemplos de su uso.

Ejemplo 1. Descarga automática de programas MQL dentro de las carpetas del terminal. Script MetaGrabber

Para empezar la prueba del funcionamiento de la clase, vamos a comenzar con las tareas más fáciles; leer la página y almacenar sus contenidos en la carpeta indicada. Pero es improbable que una simple lectura de las paginas sea muy interesante, así que para aprovechar la ejecución del script, le vamos a asignar la característica de recolección a partir de los sitios, incluida en los programas MQL. La tarea del script MetaGrabber será:

Para resolver la segunda tarea usamos la clase MqlNet. Para la tercera tarea usamos la función MoveFileEx de Kernel32.dll

#import "Kernel32.dll"
bool MoveFileExW(string &lpExistingFileName, string &lpNewFileName, int dwFlags);
#import "Kernel32.dll"

Para la primera tarea, vamos a hacer una pequeña función para analizar la línea URL.

Tenemos que asignar tres líneas a partir de la dirección: el host, la ruta al sitio y el nombre del archivo.
Por ejemplo, en la línea http://www.mysite.com/folder/page.html

- Host = www.mysite.com
- Request = / folder / page.html
- File name = page.html

En el caso de CodeBase en MQL5, las rutas tienen la misma estructura. Por ejemplo, la ruta a la librería ErrorDescription.mq5 en la página https://www.mql5.com/ru/code/79 tiene el siguiente formato http://p.mql5.com/data/18/79/ErrorDescription.mqh. Se puede obtener esta ruta fácilmente haciendo un clic derecho sobre el enlace y seleccionando "Copy Link". Por tanto, se divide la URL en dos partes, una para la petición y otra para el nombre del archivo con el fin de facilitar el almacenamiento de los archivos.

- Host = p.mql5.com
- Request = / data/18/79/5/ErrorDescription.mqh
- File name = ErrorDescription.mqh

La siguiente función ParseURL procesará el tipo de línea que se muestra a continuación.

void ParseURL(string path,string &host,string &request,string &filename)
  {
   host=StringSubstr(URL,7);
   // removed
   int i=StringFind(host,"/"); 
   request=StringSubstr(host,i);
   host=StringSubstr(host,0,i);
   string file="";
   for(i=StringLen(URL)-1; i>=0; i--)
      if(StringSubstr(URL,i,1)=="/")
        {
         file=StringSubstr(URL,i+1);
         break;
        }
   if(file!="") filename=file;
  }

Solo asignaremos dos parámetros externos del script; la URL (ruta del archivo mql5) y el tipo de carpeta para el almacenamiento posterior; es decir, en qué carpeta del terminal quiere colocarlo.

Como resultado, obtenemos un script muy pequeño pero muy útil.

//+------------------------------------------------------------------+
//|                                                  MetaGrabber.mq5 |
//|                                 Copyright © 2010 www.fxmaster.de |
//|                                         Coding by Sergeev Alexey |
//+------------------------------------------------------------------+
#property copyright "www.fxmaster.de  © 2010"
#property link      "www.fxmaster.de"
#property version               "1.00"
#property description  "Download files from internet"

#property script_show_inputs

#include <InternetLib.mqh>

#import "Kernel32.dll"
bool MoveFileExW(string &lpExistingFileName,string &lpNewFileName,int dwFlags);
#import
#define MOVEFILE_REPLACE_EXISTING 0x1

enum _FolderType
  {
   Experts=0,
   Indicators=1,
   Scripts=2,
   Include=3,
   Libraries=4,
   Files=5,
   Templates=6,
   TesterSet=7
  };

input string URL="";
input _FolderType FolderType=0;
//------------------------------------------------------------------ OnStart
int OnStart()
  {
   MqlNet INet; // variable for working in the Internet
   string Host,Request,FileName="Recieve_"+TimeToString(TimeCurrent())+".mq5";

   // parse url
   ParseURL(URL,Host,Request,FileName);

   // open session
   if(!INet.Open(Host,80)) return(0);
   Print("+Copy "+FileName+" from  http://"+Host+" to "+GetFolder(FolderType));

   // obtained file
   if(!INet.Request("GET",Request,FileName,true))
     {
      Print("-Err download "+URL);
      return(0);
     }
   Print("+Ok download "+FileName);

   // move to the target folder
   string to,from,dir;
   // if there is no need to move it elsewhere
   if(FolderType==Files) return(0);

   // from
   from=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+FileName;

   // to
   to=TerminalInfoString(TERMINAL_DATA_PATH)+"\\";
   if(FolderType!=Templates && FolderType!=TesterSet) to+="MQL5\\";
   to+=GetFolder(FolderType)+"\\"+FileName;

   // move file 
   if(!MoveFileExW(from,to,MOVEFILE_REPLACE_EXISTING))
     {
      Print("-Err move to "+to);
      return(0);
     }
   Print("+Ok move "+FileName+" to "+GetFolder(FolderType));

   return(0);
  }
//------------------------------------------------------------------ GetFolder
string GetFolder(_FolderType foldertype)
  {
   if(foldertype==Experts) return("Experts");
   if(foldertype==Indicators) return("Indicators");
   if(foldertype==Scripts) return("Scripts");
   if(foldertype==Include) return("Include");
   if(foldertype==Libraries) return("Libraries");
   if(foldertype==Files) return("Files");
   if(foldertype==Templates) return("Profiles\\Templates");
   if(foldertype==TesterSet) return("Tester");
   return("");
  }
//------------------------------------------------------------------ ParseURL
void ParseURL(string path,string &host,string &request,string &filename)
  {
   host=StringSubstr(URL,7);
   // removed
   int i=StringFind(host,"/"); 
   request=StringSubstr(host,i);
   host=StringSubstr(host,0,i);
   string file="";
   for(i=StringLen(URL)-1; i>=0; i--)
      if(StringSubstr(URL,i,1)=="/")
        {
         file=StringSubstr(URL,i+1);
         break;
        }
   if(file!="") filename=file;
  }
//+------------------------------------------------------------------+

Cómo descargar los archivos MQL

Vamos a realizar pruebas en nuestra sección favorita https://www.mql5.com/es/code. Los archivos descargados aparecerán inmediatamente en el navegador del editor, y se pueden compilar sin tener que reiniciar el terminal o el editor. Y no tendrán que recorrer los largos caminos del sistema de archivos en busca de la carpeta correspondiente para colocar los archivos en ella.

¡Atención! Muchos sitios han implementado dispositivos de seguridad para evitar la descarga masiva de contenidos, y se le puede bloquear su dirección IP en el caso de una descarga masiva. Por tanto, tenga mucho cuidado al utilizar la "máquina" para descargar automáticamente los archivos de los recursos a los que accede con frecuencia, para que no se le prohíba su uso.

Aquellos que quieran ir aún más lejos, mejorando el servicio propuesto, pueden usar el script Clipboard con la interceptación de los contenidos del portapapeles y la posterior descarga automática.

Ejemplo 2. Seguimiento de cotizaciones por varios brokers en un solo gráfico

Así que hemos aprendido cómo obtener archivos de internet. Vamos a tratar ahora un punto más interesante; cómo enviar y almacenar estos datos en el servidor. Para ello, necesitamos un pequeño script PHP adicional, que se ubicará en el servidor. Mediante la clase escrita MqlNet, creamos un Asesor Experto para el seguimiento; MetaArbitrage. La tarea del Asesor Experto junto con el script PHP será:

A continuación, se muestra el diagrama de la interacción entre el módulo MQL y el script PHP:

Usaremos la clase MqlNet para resolver estas tareas.

Para evitar la duplicidad de los datos, así como la eliminación de las cotizaciones obsoletas, vamos a enviar 4 parámetros principales: el nombre del servidor del broker (la fuente de los precios actuales), la divisa, el precio y tiempo de cotizaciones en horario UTC. El siguiente ejemplo es una petición de acceso al script desde los recursos de nuestra compañía:

www.fxmaster.de/metaarbitr.php?server=Metaquotes&pair=EURUSD&bid=1.4512&time=13286794

Se almacenan estos parámetros así como la cotización actual en el servidor y se publicarán en la página de la respuesta, con todas las demás cotizaciones de esta divisa.

El beneficio "colateral" de este intercambio es que se pueden enviar las cotizaciones desde MT5 y también desde MT4.

La página creada por el servidor es un archivo CSV. Se ve del siguiente modo en este script:

ServerName1; Bid1; Time1
ServerName 2; Bid2; Time2
ServerName 3; Bid3; Time3

ServerName N; BidN; TimeN

Pero le puede añadir sus propios parámetros adicionales (por ejemplo, el tipo de servidor; demo o real). Guardamos este archivo CSV y lo analizamos línea por línea, con el resultado en forma de una tabla de valores y líneas de precios que se muestran en la pantalla.

Se puede procesar este archivo de distintas maneras, eligiendo la manera correspondiente a cada caso. Por ejemplo, filtrar las cotizaciones recibidas del servidor de prueba de MetaTrader 4, etc.

Las ventajas del uso del servidor de internet son obvias; está enviando sus cotizaciones, las cuales puede recibir y ver otro trader. Del mismo modo, recibirá cotizaciones enviadas a otros traders. Es decir, la interacción entre los terminales es bilateral, el siguiente diagrama muestra cómo se lleva a cabo el intercambio de datos:

Este esquema sirve como base para el principio de intercambio de informaciones entre cualquier número de terminales. Se puede descargar el Asesor Experto MetaArbitrage entero y el script PHP con los comentarios a partir del enlace de los archivos adjuntos. Se puede encontrar más información sobre las funciones que usan PHP en el siguiente sitio php.su

Ejemplo 3. Intercambio de mensajes (mini chat) en el terminal. El Asesor Experto MetaChat

Despidámonos del trading y los números y vamos a crear una aplicación que nos permitirá chatear con varias personas a la vez, sin salir del terminal. Para ello, nos hará falta un script PHP más, que es muy parecido al anterior. Excepto por el hecho de que en este script, en lugar de analizar las cotizaciones temporales, analizaremos el número de líneas en un archivo. La tarea del Asesor Experto será:

El funcionamiento de MetaChat es parecido al del Asesor Experto anterior. El mismo principio y el mismo sencillo archivo de salida CSV.


MetaChat y MetaArbitrage se encuentran en el sitio de sus desarrolladores. Los scripts PHP para su funcionamiento también se encuentran ahí.
Por tanto, si quiere probar un trabajo o usar este servicio, puede hacerlo mediante el siguiente enlace:
MetaСhat - www.fxmaster.de/metachat.php
MetaArbitrage - www.fxmaster.de/metaarbitr.php

Conclusión

Así que nos hemos familiarizado con las peticiones HTTP. Hemos adquirido la capacidad de enviar y recibir datos a través de internet, y a organizar el proceso del trabajo de un modo más cómodo. Pero se puede mejorar cualquier característica, teniendo en cuento los siguientes puntos:

En este artículo hemos utilizado peticiones de tipo GET. Cumplen suficientemente con la tarea cuando necesitamos obtener un archivo o enviar una petición con pocos parámetros para los análisis del servidor.

En el siguiente trabajo, veremos más de cerca las peticiones POST; enviando archivos al servidor o compartiendo un archivo entre terminales, y veremos los ejemplos de su uso.

Recursos útiles