Descargar MetaTrader 5

Usar WinInet en MQL5. Parte 2: solicitudes y archivos POST

8 mayo 2014, 09:54
o_o
0
469

Introducción

En la lección anterior "Usar WinInet.dll para el intercambio de datos entre terminales a través de internet", hemos aprendido a trabajar con la librería, a abrir páginas web y a enviar y recibir información usando solicitudes GET.

En esta lección vamos a aprender cómo:

  • crear y enviar simples solicitudes POST a un servidor;
  • enviar archivos a un servidor usando el método de representación multipart/form-data;
  • trabajar con cookies y leer información de sitios web usando nuestra información de registro.

Como antes, recomiendo encarecidamente establecer un servidor proxy local Charles que será necesario para que pueda estudiar y realizar experimentos.


Solicitudes POST

Para enviar información necesitaremos las funciones wininet.dll y la clase creada CMqlNet que hemos descrito en detalle en el artículo anterior.

Debido al gran número de campos en los métodos CMqlNet::Request tuvimos que crear una estructura separada tagRequest que contuviera todos los campos requeridos para una solicitud. 

//------------------------------------------------------------------ struct tagRequest
struct tagRequest
{
  string stVerb;   // method of the request GET/POST/…
  string stObject; // path to an instance of request, for example "/index.htm" или "/get.php?a=1"  
  string stHead;   // request header
  string stData;   // addition string of data
  bool fromFile;   // if =true, then stData designates the name of a data file
  string stOut;    // string for receiving an answer
  bool toFile;     // if =true, then stOut designates the name of a file for receiving an answer

  void Init(string aVerb, string aObject, string aHead, 
            string aData, bool from, string aOut, bool to); // function of initialization of all fields
};
//------------------------------------------------------------------ Init
void tagRequest::Init(string aVerb, string aObject, string aHead, 
                      string aData, bool from, string aOut, bool to)
{
  stVerb=aVerb;     // method of the request GET/POST/…
  stObject=aObject; // path to the page "/get.php?a=1" or "/index.htm"
  stHead=aHead;     // request header, for example "Content-Type: application/x-www-form-urlencoded"
  stData=aData;     // addition string of data
  fromFile=from;    // if =true, the stData designates the name of a data file
  stOut=aOut;       // field for receiving an answer
  toFile=to;        // if =true, then stOut designates the name of a file for receiving an answer
}

Además, necesitamos reemplazar el encabezado del método CMqlNet::Request por uno más corto:

//+------------------------------------------------------------------+
bool MqlNet::Request(tagRequest &req)
  {
   if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED))
     {
      Print("-DLL not allowed"); return(false);
     }
//--- checking whether DLLs are allowed in the terminal
   if(!MQL5InfoInteger(MQL5_DLLS_ALLOWED))
     {
      Print("-DLL not allowed");
      return(false);
     }
//--- checking whether DLLs are allowed in the terminal
   if(req.toFile && req.stOut=="")
     {
      Print("-File not specified ");
      return(false);
     }
   uchar data[]; 
    int hRequest,hSend;
   string Vers="HTTP/1.1"; 
    string nill="";

//--- read file to array
   if(req.fromFile)
     {
      if(FileToArray(req.stData,data)<0)
        {
         Print("-Err reading file "+req.stData);
         return(false);
        }
     }
   else StringToCharArray(req.stData,data);

   if(hSession<=0 || hConnect<=0)
     {
      Close();
      if(!Open(Host,Port,User,Pass,Service))
        {
         Print("-Err Connect");
         Close();
         return(false);
        }
     }
//--- creating descriptor of the request
   hRequest=HttpOpenRequestW(hConnect,req.stVerb,req.stObject,Vers,nill,0,
   INTERNET_FLAG_KEEP_CONNECTION|INTERNET_FLAG_RELOAD|INTERNET_FLAG_PRAGMA_NOCACHE,0);
   if(hRequest<=0)
     {
      Print("-Err OpenRequest");
      InternetCloseHandle(hConnect);
      return(false);
     }
//--- sending the request
   hSend=HttpSendRequestW(hRequest,req.stHead,StringLen(req.stHead),data,ArraySize(data));
//--- sending the file
   if(hSend<=0)
     {
      int err=0;
      err=GetLastError(err);
      Print("-Err SendRequest= ",err);
     }
//--- reading the page
   if(hSend>0) ReadPage(hRequest,req.stOut,req.toFile);
//--- closing all handles
   InternetCloseHandle(hRequest); InternetCloseHandle(hSend);

   if(hSend<=0)
     {
      Close();
      return(false);
     }
   return(true);
  }

Vamos a empezar ahora a trabajar.


Enviar datos a un sitio web del tipo "application/x-www-form-urlencoded"

En la lección anterior hemos analizado el ejemplo MetaArbitrage (monitorización de cotizaciones).

Vamos a recordar que el asesor experto envía precios Bid de su símbolo usando una solicitud GET y, como respuesta, recibe precios de otros brokers que se envían de la misma forma al servidor desde otros terminales.

Para convertir una solicitud GET en otra POST es suficiente "ocultar" la línea de solicitud en el cuerpo de la propia solicitud que viene después del encabezado.

BOOL HttpSendRequest(
  __in  HINTERNET hRequest,
  __in  LPCTSTR lpszHeaders,
  __in  DWORD dwHeadersLength,
  __in  LPVOID lpOptional,
  __in  DWORD dwOptionalLength
);

  • hRequest [in]
    Controlador devuelto por HttpOpenRequest.
  • lpszHeaders [in]
    Puntero a una línea que contiene los encabezados a añadir a la solicitud. Este parámetro puede estar vacío.
  • dwHeadersLength [in]
    Tamaño del encabezado en bytes.
  • lpOptional [in]
    Puntero a una matriz con datos uchar que se establecen después del encabezado. Por lo general, este parámetro se usa para las operaciones POST y PUT.
  • dwOptionalLength [in]
    Tamaño de los datos en bytes. El parámetro puede ser 0, y significa que no se ha enviado ninguna información.

A partir de la descripción de la función podemos comprender que los datos se envían como una matriz uchar de byte (el cuarto parámetro de la función). Esto es todo lo que debemos saber en esta fase.

En el ejemplo de MetaArbitrage la solicitud GET es la siguiente:

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


La propia solicitud se destaca en color rojo. Por ello, si necesitamos realizar una solicitud POST debemos mover su texto a la matriz de datos lpOptional.

Vamos a crear un script llamado MetaSwap que enviará y recibirá información sobre los swaps de un símbolo. 

#include <InternetLib.mqh>

string Server[];        // array of server names
double Long[], Short[]; // array for swap information
MqlNet INet;           // class instance for working

//------------------------------------------------------------------ OnStart
void OnStart()
{
//--- opening a session
  if (!INet.Open("www.fxmaster.de", 80, "", "", INTERNET_SERVICE_HTTP)) return;
 
//--- zeroizing arrays
  ArrayResize(Server, 0); ArrayResize(Long, 0); ArrayResize(Short, 0);
//--- the file for writing an example of swap information
  string file=Symbol()+"_swap.csv";
//--- sending swaps
  if (!SendData(file, "GET")) 
  { 
    Print("-err RecieveSwap"); 
    return; 
  }
//--- read data from the received file
  if (!ReadSwap(file)) return; 
//--- refresh information about swaps on the chart
  UpdateInfo();               
}

La operación del script es muy simple.

En primer lugar se abre la sesión de internet INet.Open. Luego la función SendData envía información sobre los swaps del símbolo actual. A continuación, si se envía con éxito, se leen los swaps recibidos usando ReadSwap y los mostrados en el gráfico mediante UpdateInfo.

En este momento nos interesa solo la función SendData.

//------------------------------------------------------------------ SendData bool SendData(string file, string mode) {   string smb=Symbol();   string Head="Content-Type: application/x-www-form-urlencoded"; // header   string Path="/mt5swap/metaswap.php"; // path to the page   string Data="server="+AccountInfoString(ACCOUNT_SERVER)+               "&pair="+smb+               "&long="+DTS(SymbolInfoDouble(smb, SYMBOL_SWAP_LONG))+               "&short="+DTS(SymbolInfoDouble(smb, SYMBOL_SWAP_SHORT));   tagRequest req; // initialization of parameters   if (mode=="GET")  req.Init(mode, Path+"?"+Data, Head, "",   false, file, true);   if (mode=="POST") req.Init(mode, Path,          Head, Data, false, file, true);   return(INet.Request(req)); // sending request to the server }

En este script se incluyen dos métodos de envío de información usando GET y POST para que aprecie la diferencia entre ambos.

Vamos a describir las variables de la función una a una:

  • Head - encabezado de la solicitud que describe el tipo del contenido de esta. Realmente, este no es todo el encabezado de la solicitud. Los demás campos del encabezado son creados por la librería wininet.dll. No obstante, pueden modificarse usando la función HttpAddRequestHeaders .
  • Path - esta es la ruta hacia la instancia de la solicitud en relación con el dominio inicial www.fxmaster.de. En otras palabras, es la ruta a un script php que procesará la solicitud. A propósito, no es necesario solicitar solo un script php, puede ser una página html ordinaria (hemos intentado solicitar un archivo mq5 durante nuestra primera lección.
  • Data - esta es la información enviada al servidor. Los datos se escriben según las reglas que determinan cómo se pasa parameter name=value. El símbolo "&" se usa como separador de datos.

Y lo más importante: preste atención a la diferencia entre realizar una solicitud GET y POST en tagRequest::Init.

En el método GET la ruta se envía junto con el cuerpo de la solicitud (unidos con el símbolo "?") y se deja vacío el campo de datos lpOptional (con el nombre stData en la estructura).
En el método POST, la ruta existe por sí misma, y el cuerpo de la solicitud se desplaza a lpOptional.

Como puede ver, la diferencia no es significativa. En el script del servidor que recibe la solicitud se adjunta este archivo.


Enviar datos "multipart/form-data"

En realidad, las solicitudes POST no son análogas a las solicitudes GET (ya que de lo contrario no serían necesarias). Las solicitudes POST tienen una ventaja significativa: al usarlas podemos enviar archivos con contenido binario.

La cuestión es que una solicitud del tipo urlencoded puede enviar un conjunto limitado de símbolos. De lo contrario los símbolos no permitidos serían sustituidos con código. De esta forma, al enviar datos binarios estos serán desvirtuados. Luego no puede enviar ni tan siquiera un pequeño archivo gif usando una solicitud GET.

Para resolver este problema, se utilizan reglas especiales para describir una solicitud que permiten el intercambio mediante archivos binarios además de los de texto.

Para conseguir este objetivo, el cuerpo de la solicitud se divide en dos secciones. Lo importante es que cada sección puede tener sus propios tipos de datos. Por ejemplo, la primera es un texto, la siguiente es una imagen, etc. En otras palabras, una solicitud enviada al servidor puede contener varios tipos de datos al mismo tiempo.

Vamos a ver la estructura de dicha descripción mediante el ejemplo de los datos pasados por el script MetaSwap.

El encabezado de la solicitud Head será el siguiente:

Content-Type: multipart/form-data; boundary=SEPARATOR\r\n


La palabra clave SEPARATOR es un conjunto aleatorio de símbolos. No obstante, debe procurar que esté fuera de los datos de la solicitud. En otras palabras, esta línea debe ser única, algún tipo de abracadabra como hdsJK263shxaDFHLsdhsDdjf9 o cualquier otra cosa que le venga a la mente :). En PHP, dicha línea se forma usando el código MD5 de un momento actual.

La propia solicitud POST es de la siguiente forma (para facilitar su comprensión los campos se han marcado según su significado general):

\r\n
--SEPARATOR\r\n

Content-Disposition: form-data; name="Server"\r\n
\r\n
MetaQuotes-Demo

\r\n
--SEPARATOR\r\n

Content-Disposition: form-data; name="Pair"\r\n
\r\n
EURUSD

\r\n
--SEPARATOR\r\n

Content-Disposition: form-data; name="Long"\r\n
\r\n
1.02

\r\n
--SEPARATOR\r\n

Content-Disposition: form-data; name="Short"\r\n
\r\n
-0.05

\r\n
--SEPARATOR--\r\n


Especificamos de forma explícita los lugares para las fuentes de suministro "\r\n" ya que son símbolos obligatorios en una solicitud. Como puede ver, los mismos cuatro campos se pasan en la solicitud y en ellos se lleva a cabo en la forma habitual.

Peculiaridades importantes de la ubicación de los separadores:

  • Se sitúan dos símbolos "--" antes del separador.
  • Para el separador de cierre se añaden dos símbolos adicionales "--" después del mismo.


En el siguiente ejemplo puede ver un método correcto de pasar los archivos en una solicitud.

Imagine que un asesor experto realiza una instantánea y un informe detallado sobre la cuenta en un archivo de texto al cerrar una posición.

\r\n
--SEPARATOR\r\n

Content-Disposition: form-data; name="ExpertName"\r\n
\r\n
MACD_Sample

\r\n
--SEPARATOR\r\n

Content-Disposition: file; name="screen"; filename="screen.gif"\r\n
Content-Type: image/gif\r\n
Content-Transfer-Encoding: binary\r\n
\r\n
......content of the gif file.....

\r\n
--SEPARATOR\r\n

Content-Disposition: form-data; name="statement"; filename="statement.csv"\r\n
Content-Type: application/octet-stream\r\n
Content-Transfer-Encoding: binary\r\n
\r\n
......content of the csv file.....

\r\n
--SEPARATOR--\r\n


En la solicitud aparecen dos nuevos encabezados:

Content-Type describe el tipo de contenido. Todos los tipos posibles se describen con precisión en la norma RFC[2046]. Hemos usado dos tipos: image/gif y application/octet-stream

Dos variantes de la escritura de Content-Disposition: el archivo y los datos del formulario son equivalentes y son procesados correctamente por PHP en ambos casos. Por lo que puede usar un archivo o datos de formulario según desee. Puede ver mejor la diferencia entre sus representaciones en Charles.

Content-Transfer-Encoding describe la codificación del contenido. Puede estar ausente para los datos de texto.

Para consolidar el material vamos a escribir el script ScreenPost que envía la captura de pantalla al servidor:

#include <InternetLib.mqh>

MqlNet INet; // class instance for working

//------------------------------------------------------------------ OnStart
void OnStart()
{
  // opening session
  if (!INet.Open("www.fxmaster.de", 80, "", "", INTERNET_SERVICE_HTTP)) return;

  string giffile=Symbol()+"_"+TimeToString(TimeCurrent(), TIME_DATE)+".gif"; // name of file to be sent
 
  // creating screenshot 800х600px
  if (!ChartScreenShot(0, giffile, 800, 600)) { Print("-err ScreenShot "); return; }
 
  // reading gif file to the array
  int h=FileOpen(giffile, FILE_ANSI|FILE_BIN|FILE_READ); if (h<0) { Print("-err Open gif-file "+giffile); return; }
  FileSeek(h, 0, SEEK_SET);
  ulong n=FileSize(h); // determining the size of file
  uchar gif[]; ArrayResize(gif, (int)n); // creating uichar array according to the size of data
  FileReadArray(h, gif); // reading file to the array
  FileClose(h); // closing the file
 
  // creating file to be sent
  string sendfile="sendfile.txt";
  h=FileOpen(sendfile, FILE_ANSI|FILE_BIN|FILE_WRITE); if (h<0) { Print("-err Open send-file "+sendfile); return; }
  FileSeek(h, 0, SEEK_SET);

  // forming a request
  string bound="++1BEF0A57BE110FD467A++"; // separator of data in the request
  string Head="Content-Type: multipart/form-data; boundary="+bound+"\r\n"; // header
  string Path="/mt5screen/screen.php"; // path to the page
 
  // writing data
  FileWriteString(h, "\r\n--"+bound+"\r\n");
  FileWriteString(h, "Content-Disposition: form-data; name=\"EA\"\r\n"); // the "name of EA" field
  FileWriteString(h, "\r\n");
  FileWriteString(h, "NAME_EA");
  FileWriteString(h, "\r\n--"+bound+"\r\n");
  FileWriteString(h, "Content-Disposition: file; name=\"data\"; filename=\""+giffile+"\"\r\n"); // field of the gif file
  FileWriteString(h, "Content-Type: image/gif\r\n");
  FileWriteString(h, "Content-Transfer-Encoding: binary\r\n");
  FileWriteString(h, "\r\n");
  FileWriteArray(h, gif); // writing gif data
  FileWriteString(h, "\r\n--"+bound+"--\r\n");
  FileClose(h); // closing the file

  tagRequest req; // initialization of parameters
  req.Init("POST", Path, Head, sendfile, true, "answer.htm", true);
 
  if (INet.Request(req)) Print("-err Request"); // sending the request to the server
  else Print("+ok Request");
} 

Script del servidor que recibe información:

<?php
$ea=$_POST['EA'];
$data=file_get_contents($_FILES['data']['tmp_name']); // information in the file
$file=$_FILES['data']['name'];
$h=fopen(dirname(__FILE__)."/$ea/$file", 'wb'); // creating a file in the EA folder
fwrite($h, $data); fclose($h); // saving data
?>

Es muy recomendable familiarizarse con las reglas de recepción de archivos por parte del servidor ¡para evitar problemas de seguridad!


Trabajando con cookies

Trataremos esta materia brevemente de forma adicional a la lección anterior y como material de reflexión sobre sus características.

Como sabe, las cookies tienen por finalidad evitar solicitudes constantes de detalles personales por los servidores. Una vez que el servidor recibe la información personal necesaria para la sesión de trabajo actual de un usuario, este deja un archivo de texto con dicha información en el ordenador del usuario. Además, cuando el usuario se desplaza entre páginas, el servidor no solicita esa información de nuevo al usuario, sino que automáticamente la toma de la caché del navegador.

Por ejemplo, cuando habilita la opción "Recordármelo" al autorizar al servidor de www.mql5.com, se guarda una cookie con su información en su ordenador. En la próxima visita a la página web, el navegador pasará la cookie al servidor sin preguntarle.

Si está interesado, puede abrir la carpeta (WinXP) C:\Documents and Settings\<User>\Cookies y ver el contenido de las distintas páginas web que ha visitado.

Para lo que necesitamos, las cookies pueden usarse para leer nuestras páginas del foro de MQL5. En otras palabras, podrá leer la información como si estuviera autorizado en el sitio web bajo su información de registro, y luego podrá analizar las páginas obtenidas. La variante óptima es analizar las cookies usando un servidor proxy local Charles. Muestra información detallada sobre todas las solicitudes recibidas/enviadas, incluyendo las cookies.

Por ejemplo:

  • Un asesor experto (o una aplicación externa) que envía una solicitud a la página https://www.mql5.com/es/job una vez cada hora y recibe la lista de nuevas ofertas de trabajo.
  • También solicita una sección, por ejemplo https://www.mql5.com/en/forum/53, y comprueba si hay nuevos mensajes.
  • Además, puede comprobar si hay nuevos "mensajes privados" en los foros.

Para establecer una cookie en una solicitud se utiliza la función InternetSetCookie .

BOOL InternetSetCookie(
  __in  LPCTSTR lpszUrl,
  __in  LPCTSTR lpszCookieName,
  __in  LPCTSTR lpszCookieData
);

  • lpszUrl [in] - Nombre del servidor, por ejemplo, www.mql5.com
  • lpszCookieName [in]- Nombre de una cookie
  • lpszCookieData [in] - Datos de la cookie

Para establecer varias cookies llame a esta función para cada una de ellas.

Una característica importante: puede realizarse una llamada a InternetSetCookie cada vez, incluso si no estamos conectados al servidor.


Conclusión

Nos hemos familiarizado con otro tipo de solicitudes HTTP y hemos tenido la oportunidad de enviar archivos binarios, lo que permite ampliar las posibilidades del trabajo con nuestros servidores, y hemos aprendido los métodos del trabajo con cookies.

Podemos realizar la siguiente lista de instrucciones para desarrollos adicionales:

  • Organización de almacenamiento remoto de informes;
  • Intercambio de archivos entre usuarios, actualización de versiones de asesores expertos/indicadores;
  • Creación de escáneres personalizados que funcionan en nuestra cuenta y monitorizan la actividad en un sitio web.


Enlaces útiles

  1. Un servidor proxy para ver encabezados enviados - http://www.charlesproxy.com/
  2. Descripción de WinHTTP - http://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
  3. Descripción de la sesión HTTP - http://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
  4. El kit de herramientas de Denwer para la instalación local de Apache+PHP - http://www.denwer.ru/
  5. Tipos de encabezados de solicitud - http://www.codenet.ru/webmast/php/HTTP-POST.php#part_3_2
  6. Tipos de solicitudes - http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
  7. Tipos de solicitudes - ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/media-types.
  8. Estructura del uso de HINTERNET - http://msdn.microsoft.com/en-us/library/aa383766%28VS.85%29.aspx
  9. Trabajo con archivos - http://msdn.microsoft.com/en-us/library/aa364232%28VS.85%29.aspx
  10. Tipos de datos a pasar a MQL - http://msdn.microsoft.com/en-us/library/aa383751%28VS.85%29.aspx


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

Archivos adjuntos |
metaswap.zip (0.66 KB)
screenpost.zip (0.33 KB)
internetlib.mqh (12.82 KB)
screenpost.mq5 (2.63 KB)
metaswap.mq5 (4.84 KB)
Documentación generada automáticamente para el código de MQL5 Documentación generada automáticamente para el código de MQL5

La mayoría de desarrolladores de código Java estarán familiarizados con la documentación generada automáticamente que puede obtenerse con JavaDocs. La idea es añadir comentarios al código de forma semiestructurada para que pueda ser extraída en forma de una archivo de ayuda por el que sea fácil navegar. El mundo de C++ tiene también una serie de autogeneradores de documentación, siendo líderes SandCastle y Doxygen, ambos de Microsoft. El artículo describe el uso de Doxygen para crear archivos de ayuda de html a partir de comentarios estructurados en código MQL5. El experimento funcionó muy bien y creo que la documentación de ayuda que ofrece Doxygen a partir del código MQL5 añadirá una gran cantidad de valor a este.

Fundamentos de programación en MQL5 - Tiempo Fundamentos de programación en MQL5 - Tiempo

En este artículo vamos a analizar las funciones estándar MQL5 que se utilizan para trabajar con el tiempo, veremos las técnicas de programación y otras funciones muy útiles a la hora de trabajar con el tiempo y las que necesitaremos durante la creación de Asesores Expertos e indicadores. Además, dedicaremos bastante atención a la teoría general del sistema cronológico. En primer lugar, este artículo puede ser interesante para los principiantes que se han puesto a estudiar la programación en MQL5.

Aplicar la transformada de Fisher y su transformada inversa al análisis de mercado en Meta Trader 5 Aplicar la transformada de Fisher y su transformada inversa al análisis de mercado en Meta Trader 5

Ahora sabemos que la función densidad de probabilidad (PDF) de un ciclo de mercado no recuerda a una gausiana sino más bien a una PDF de una onda senoidal y la mayoría de indicadores asumen que la PDF del ciclo del mercado es gausiana, por lo que necesitamos conocer una forma de "corregir" eso. La solución es usar la transformada de Fisher. La transformada de Fisher cambia una PDF de cualquier forma de onda en otra aproximadamente gausiana. Este artículo describe las matemáticas que hay tras la transformada de Fisher y la transformada inversa de Fisher y su aplicación al trading. Se presenta y evalúa un módulo de señal de trading propio basado en la transformada de Fisher inversa.

Cómo desarrollar un asesor experto usando las herramientas de UML Cómo desarrollar un asesor experto usando las herramientas de UML

Este artículo trata sobre la creación de asesores expertos utilizando el lenguaje gráfico UML, usado para el modelado visual de sistemas de software orientados a objetos. La principal ventaja de este enfoque es la visualización del proceso de modelado. El artículo contiene un ejemplo que muestra el modelado de la estructura y propiedades de un asesor experto usando el modelador de ideas de software.