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