Die Verwendung von WinInet in MQL5 Teil 2: POST-Anfragen und -Dateien

--- | 4 Mai, 2016

Einleitung

In der vorigen Lektion, "Using WinInet.dll for Data Exchange between Terminals via the Internet (Die Verwendung von WinInet.dll für den Datenaustausch zwischen Terminals mittels Internet)", haben wir gelernt,wie man mit GET-Anfragen mit der Bibliothek arbeitet, Webseiten öffnet und Informationen sendet und empfängt.

In dieser Lektion lernen wir:

  • wie man einfache POST-Anfragen erstellt und an einen Server schickt
  • wie man Daten mit der multipart/form-data-Methode an einen Server schickt
  • wie man mit Cookies arbeitet und Informationen einer Webseite mit seinem Login liest.

Ich empfehle Ihnen hier nochmals ausdrücklich, den lokalen Proxy Server Charles zu installieren, der für weiterführendes Lernen und Experimente notwendig sein wird.

POST-Anfragen

Um Informationen zu verschicken, brauchen wir die bereits im vorigen Beitrag beschriebenen Wininet.dll-Funktionen, sowie die erstellte Klasse CMqlNet.

Aufgrund der großen Anzahl von Feldern in der CMqlNet::Request-Methode mussten wir die neue Struktur tagRequest erstellen, die alle für eine Anfrage erforderlichen Felder beinhaltet.  

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

Außerdem müssen wir den Titel der CMqlNet::Request-Methode mit einem kürzeren ersetzen.

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

Also dann, los geht's!



Daten an eine Webseite des Typus "application/x-www-form-urlencoded" schicken

In der vorigen Lektion wurde das Beispiel MetaArbitrage (überwachen von Geboten) analysiert.

Erinnern wir uns daran, dass der Expert Advisor mit einer GET-Anfrage Gebotspreise verschickt, und als Antwort bekommt er Preise anderer Broker, die über den gleichen Weg vom Server zu anderen Terminals gesendet werden.

Um eine GET-Anfrage in eine POST-Anfrage zu verwandeln, reicht es, die Anfragezeile selbst im Text der Anfrage zu "verstecken", der nach dem Header kommt.

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

  • hRequest [in]
    Ausführung zurück von HttpOpenRequest
  • lpszHeaders [in]
    Zeiger auf eine Zeile, die den in die Anfrage aufzunehmenden Header beinhaltet Dieser Parameter kann leer sein.
  • dwHeadersLength [in]
    Größe des Headers in Bytes.
  • lpOptional [in]
    Zeiger auf ein Array mit uchar-Daten, das gleich nach dem Header gesendet wird. Im Allgemeinen wird dieser Parameter für POST- und PUT-Befehle verwendet.
  • dwOptionalLength [in]
    Größe der Daten in Bytes. Der Parameter kann =0 sein, das heißt, dass keine zusätzliche Information gesendet wird.

Von der Beschreibung der Funktion können wir schlussfolgern, dass die Daten als Byte-uchar-Array gesendet werden (der vierte Parameter der Funktion). Mehr müssen wir an dieser Stelle noch nicht wissen.

Im MetaArbitrage-Beispiel schaut die GET-Anfrage folgendermaßen aus:

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

Die Anfrage selbst ist in Rot hervorgehoben. Das heißt also, dass wenn wir eine POST-Anfrage stellen müssen, wir ihren Text ins lpOptional-Datenarray eingeben sollten.

Nun werden wir ein Skript mit dem Namen MetaSwap erstellen, das Informationen über Swaps eines Symbols senden und empfangen kann.  

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

Die Umsetzung dieses Skriptes ist äußerst einfach.

Erstens muss man sichergehen, dass die Internet Session INet.Open geöffnet ist. Dann sendet die SendData-Funktion Informationen über Swaps des aktuellen Symbols. Nachdem das Senden erfolgreich war, werden die empfangenen Swaps mit ReadSwap gelesen und mit UpdateInfo auf der Chart angezeigt.

Zu diesem Zeitpunkt sind wir lediglich an der SendData-Funktion interessiert.

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

Hier werden zwei Methoden, wie man Informationen senden kann, gezeigt: mit GET und POST, damit man ein Gefühl für die Unterschiede der beiden bekommt.

Die Variablen der Funktionen werden hier nacheinander beschreiben:

  • Head - Header, oder Kopfzeile, der Anfrage; beschreibt ihren Inhalt. Das ist eigentlich nicht der gesamte Header der Anfrage. Die anderen Felder des Headers werden von der Bibliothek Wininet.dll erstellt. Sie können aber anhand der HttpAddRequestHeaders-Funktion verändert werden.
  • Path - dies ist der Dateipfad (path) zur Instanz, die die Anfrage sendet in Bezug auf die Domain vom Anfang, www.fxmaster.de. Anders ausgedrückt, dies ist der Pfad zu einem PHP-Skript, das die Anfrage bearbeiten wird. Übrigens ist es nicht notwendig, nur ein PHP-Skript anzufragen, es kann sich auch um eine gewöhnliche html-Seite handeln (in der ersten Lektion haben wir sogar versucht, eine mq5-Datei anzufragen).
  • Data - dies ist die Information, die zu den Servern geschickt wird. Data wird gemäß den Regeln parameter name=value geschrieben. Das "&"-Zeichen wird als Daten-Separator verwendet.

Und am wichtigsten: Beachten Sie den Unterschied zwischen GET- und POST-Anfragen bei tagRequest::Init.

Bei der GET-Methode wird der Pfad zusammen mit der gesamten Anfrage (zusammen mit dem "?"-Zeichen) gesendet, und das Datenfeld lpOptional (in der Struktur stData genannt) wird leer gelassen.
Bei der POST-Methode
gibt es den Pfad für sich alleine und die gesamte Anfrage wird nach lpOptional verschoben.

Wie Sie also sehen können, ist der Unterschied nicht sehr ausschlaggebend. Das Server-Skript metaswap.php, das die Anfrage erhält, ist an diesen Beitrag angehängt.


Das Senden von "multipart/form-data" Daten

POST-Anfragen sind nicht genau das gleiche wie GET-Anfragen (sonst wären sie auch nicht notwendig). Der entscheidende Vorteil von POST-Anfragen ist, dass man mit ihnen Dateien mit binären Inhalten verschicken kann.

URL-encodierte Anfragen dürfen nur eine bestimmte Anzahl von Zeichen haben. Andernfalls werden die überschüssigen Zeichen durch Codes ersetzt. Das heißt, wenn man Binärdateien verschickt, werden diese verfälscht. Folglich können Sie nicht einmal eine kleine gif-Datei mit einer GET-Anfrage schicken.

Um dieses Problem zu lösen, wurden spezielle Regeln für das Schreiben einer Anfrage entwickelt, die einen Austausch von Binärdateien zusätzlich zu Textdateien ermöglichen.

Um das zu erreichen, wird die Anfrage in Abschnitte geteilt. Das wichtigste ist, dass jeder einzelne Abschnitt seine eigenen Datentypen hat. Der erste ist zum Beispiel Text, der nächste ist ein Bild im .jpeg-Format, etc. Anders ausgedrückt kann eine Anfrage, die zum Server geschickt wird, mehrere verschiedene Arten von Daten auf einmal enthalten.

Schauen wir uns nun die Struktur solch einer Beschreibung am Beispiel der Daten vom MetaSwap-Skript an.

Der Header der Anfrage Head wird folgendermaßen aussehen:

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

Das Schlüsselwort SEPARATOR ist eine zufällige Auswahl an Zeichen. Sie sollten jedoch aufpassen, dass dies nicht innerhalb der Anfrage geschrieben wird. Diese Zeile sollte also einzigartig sein - etwas wie hdsJK263shxaDFHLsdhsDdjf9 oder was auch immer Ihnen sonst einfällt :). Bei PHP wird eine solche Zeile mithilfe eines aktuellen MD5-Codes erstellt.

Die POST-Anfrage selbst schaut folgendermaßen aus (die Felder sind für besseres Verständnis hervorgehoben):

\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


Ohne die Befehle "\r\n" am Ende einer Anfrage kann sie nicht verarbeitet werden. Wie Sie sehen, werden also die gleichen vier Felder in dieser Anfrage verarbeitet, und zwar auf die übliche Weise mit Text.

Separatoren hinzufügen:

  • Die zwei Zeichen "--" werden vor den Separator geschrieben.
  • Beim Schluss-Seperator werden noch zwei zusätzliche Zeichen "--" hinzugefügt.

Im nächsten Beispiel sieht man, wie Dateien richtig an Anfragen hinzugefügt werden.

Stellen Sie sich vor, dass ein Expert Advisor einen Screenshot der Chart macht und einen detaillierten Bericht über den Account in einer Textdatei schreibt während er eine Position schließt.

\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


Zwei neue Header tauchen in der Anfrage auf:

Content-Type - beschreibt die Art des Inhalts. Alle möglichen Typen sind genau im RFC[2046]-Standard beschrieben. Es wurden hier zwei Typen verwendet: image/gif und application/octet-stream.

Zwei Schreibmöglichkeiten von Content-Disposition - Datei- und Formulardaten sind gleichwertig und wurden beide von PHP korrekt verarbeitet. Sie können also zwischen Datei- und Formulardaten auswählen. Ihre jeweiligen Unterschiede sind mit dem Proxy-Server Charles besser erkennbar.

Content-Transfer-Encoding - beschreibt die Enkodierung des Inhalts. Textdaten könnten nicht vorhanden sein.

Schreiben wir nun das Skript ScreenPost, das Screenshots zum Server schickt.

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

Das Server-Skript empfängt Information:

<?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 wird nachdrücklich empfohlen, sich die Regeln des Dateiempfanges von Seiten der Server anzueignen, um Sicherheitsprobleme zu verhindern!

Arbeiten mit Cookies

Dieses Thema wird kurz als eine Erweiterung der vorigen Lektion behandelt, und damit man über die Eigenschaften nachdenken kann.

Cookies gibt es, damit Server nicht immer wiederholt nach persönlichen Daten fragen müssen. Sobald ein Server die Daten eines Users bekommt, die er für die aktuelle Arbeitssession benötigt, wird eine Textdatei mit dieser Information auf dem Computer des Users hinterlassen. Wenn nun der User von einer Seite auf die nächste wechselt, muss der Server diese Information nicht noch einmal anfragen, da er sie automatisch vom Browser-Cache beziehen kann.

Wenn man zum Beispiel die Option "auf diesem Computer merken" ("remember me") ermöglicht während man sich auf www.mql5.com einloggt, speichert man ein Cookie mit seinen Daten auf seinem Computer. Beim nächsten Besuch wird der Browser das Cookie an den Server weiterreichen, ohne den User noch einmal zu fragen.

Wenn Ihr Interesse geweckt ist, können Sie den Ordner (WinXP) C:\Documents and Settings\<User>\Cookies öffnen und die Inhalte der verschiedenen Webseiten, die Sie besucht haben, einsehen.

Cookies können verwendet werden, um Ihre Seiten im MQL5-Forum zu lesen. Sie werden die Informationen also mit der Autorisierung Ihres Login lesen und können dann die erhaltenen Seiten analysieren. Am besten ist es, die Cookies mit dem lokalen Proxy Server Charles zu analysieren. Er zeigt detaillierte Informationen über alle empfangenen und gesendeten Anfragen, auch Cookies.

Zum Beispiel:

  • Ein Expert Advisor (oder eine externe Anwendung), der die Seite https://www.mql5.com/de/job einmal in der Stunde anfragt und die Liste von neuen Jobangeboten empfängt.
  • Er fragt auch eine Nebenseite an, zum Beispiel https://www.mql5.com/en/forum/53, und überprüft, ob es dort neue Nachrichten gibt.
  • Außerdem überprüft er, ob es neue "Privatnachrichten" in den Foren gibt.

Um ein Cookie in eine Anfrage zu bauen, wird die Funktion InternetSetCookie verwendet.

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

  • lpszUrl [in] - Name eines Servers, zum Beispiel www.mql5.com
  • lpszCookieName [in] - Name eines Cookie
  • lpszCookieData [in] - Daten des Cookie

Um mehrere Cookies einzubauen, wird dieser Befehl für jedes einzelne von ihnen ausgeführt.

Eine interessante Eigenschaft: der Befehl InternetSetCookie kann jederzeit ausgeführt werden, auch wenn man nicht mit dem Internet verbunden ist.


Fazit

Nun haben wir also eine weitere Art der HTTP-Anfrage gelernt, wissen, wie man Binärdateien verschickt, was uns hilft, mit unseren Servern zu arbeiten, und wir haben gelernt, wie man mit Cookies arbeitet.

Folgende Möglichkeiten stehen uns nun zur weiteren Entwicklung zur Verfügung:

  • Organisation der Fernspeicherung von Berichten;
  • Dateienaustausch zwischen Usern und Aktualisierung der Versionen des Expert Advisors/Indikators;
  • Erstellung von benutzerdefinierten Scannern, die unter Ihrem Account arbeiten und Aktivitäten auf der Webseite überwachen.


Nützliche Links

  1. Ein Proxy-Server um gesendete Header anzuschauen - http://www.charlesproxy.com/
  2. Beschreibung von WinHTTP - http://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
  3. Beschreibung von HTTP Session - http://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
  4. Das Denwer-Toolkit für eine lokale Installierung von Apache+PHP - http://www.denwer.ru/
  5. Arten von Anfrage-Headern - http://www.codenet.ru/webmast/php/HTTP-POST.php#part_3_2
  6. Anfragearten - http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
  7. Anfragearten - ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/media-types.
  8. Structure of using HINTERNET - http://msdn.microsoft.com/en-us/library/aa383766%28VS.85%29.aspx
  9. Mit Dateien arbeiten - http://msdn.microsoft.com/en-us/library/aa364232%28VS.85%29.aspx
  10. Dateitypen, die - http://msdn.microsoft.com/en-us/library/aa383751%28VS.85%29.aspx