MQL5'te WinInet'i Kullanma. Bölüm 2: POST İstekleri ve Dosyalar

--- | 22 Aralık, 2021


Giriş

Bir önceki "İnternet üzerinden Terminaller Arası Veri Alışverişi için WinInet.dll Kullanımı" adlı makalede, kitaplık ile çalışmayı, web sayfalarını açmayı, GET isteklerini kullanarak bilgi göndermeyi ve almayı öğrenmiştik.

Bu makalede, şunları yapmayı öğreneceğiz:

Daha önce olduğu gibi, yerel bir proxy sunucusu kurmanızı şiddetle tavsiye ediyorum Charles; bu, çalışmanız ve sonraki denemeleriniz için gerekli olacaktır.


POST İstekleri

Bilgi göndermek için, önceki makalede ayrıntılı olarak açıklanan wininet.dll işlevlerine ve oluşturulan CMqlNet sınıfına ihtiyacımız olacak.

CMqlNet::Request yöntemlerindeki çok sayıda alan nedeniyle, bir istek için gerekli tüm alanları içeren ayrı bir tagRequest yapısı oluşturmamız gerekiyordu. 

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

Ayrıca, CMqlNet::Request yönteminin başlığını daha kısa bir başlıkla değiştirmemiz gerekiyor:

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

O halde çalışmaya başlayalım...


"application/x-www-form-urlencoded" Türündeki Bir Web Sitesine Veri Gönderme

Önceki derste, MetaArbitrage örneğini (fiyat tekliflerinin izlenmesi) analiz etmiştik.

EA'nın bir GET isteği kullanarak sembolünün Alış fiyatlarını gönderdiğini ve cevap olarak diğer terminallerden sunucuya aynı şekilde gönderilen diğer aracıların fiyatlarını aldığını hatırlayalım.

Bir GET isteğini bir POST isteğine dönüştürmek için, başlığından sonra gelen isteğin gövdesinde istek satırının kendisini "gizlemek" yeterlidir.

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

  • hRequest [in]
    HttpOpenRequest tarafından döndürülen tanıtıcı.
  • lpszHeaders [in]
    İsteğe eklenecek başlıkları içeren bir satırın işaretçisi. Bu parametre boş olabilir.
  • dwHeadersLength [in]
    Başlığın bayt cinsinden boyutu.
  • lpOptional [in]
    Başlıktan hemen sonra gönderilen uchar verilerini içeren bir diziye işaretçi. Genellikle bu parametre POST ve PUT işlemleri için kullanılır.
  • dwOptionalLength [in]
    Bayt cinsinden veri boyutu. Parametre =0 olabilir; bu, ek bilgi gönderilmediği anlamına gelir.

İşlevin açıklamasından, verilerin bir baytlık uchar-dizisi (işlevin dördüncü parametresi) olarak gönderildiğini anlayabiliriz. Bu aşamada bilmemiz gereken tek şey bu.

MetaArbitrage örneğinde, GET isteği aşağıdaki gibi görünür:

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


İsteğin kendisi kırmızı renkle vurgulanır. Bu nedenle, bir POST isteği yapmamız gerekirse, metnini lpOptional veri dizisine taşımalıyız.

Bir sembolün swap'ları hakkında bilgi gönderecek ve alacak olan MetaSwap adlı bir script dosyası oluşturalım. 

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

Script dosyasının çalışması çok basittir.

Öncelikle INet.Open İnternet oturumu açılır. Ardından SendData işlevi, mevcut sembolün swap'ları hakkında bilgi gönderir. Bundan sonra, başarılı bir şekilde gönderilirse, alınan swap'lar ReadSwap kullanılarak okunur ve UpdateInfo kullanılarak grafikte görüntülenir.

Şu anda, yalnızca SendData işleviyle ilgileniyoruz.

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

Bu script dosyasında, bilgi göndermenin iki yöntemi gösterilmektedir - Bu, aralarındaki farkı anlamanız için GET ve POST kullanılarak yapılmıştır.

İşlevin değişkenlerini tek tek tanımlayalım:

Ve asıl mesele - tagRequest::Init'de GET ve POST istekleri yapmak arasındaki farka dikkat edin.

GET yönteminde, yol istek gövdesiyle birlikte ("?" işaretiyle birleştirilmiş) gönderilir ve veri alanı lpOptional (yapıda stData olarak adlandırılmış) boş bırakılır.
POST yönteminde
, yol kendi başına bulunur ve istek gövdesi lpOptional öğesine taşınır.

Gördüğünüz gibi, fark önemli değildir. İsteği alan metaswap.php sunucu script dosyası makaleye eklenmiştir.


"Multipart/form-data" Verilerini Gönderme

Aslında, POST istekleri GET isteklerine benzemez (aksi takdirde bunlara ihtiyaç duyulmazdı). POST isteklerinin önemli bir avantajı vardır; bunları kullanarak ikili içerikli dosyalar gönderebilirsiniz.

Mesele, urlencoded türünün bir isteğinin sınırlı sayıda sembol göndermesine izin verilmesidir. Aksi takdirde, "izin verilmeyen" semboller kodlarla değiştirilecektir. Dolayısıyla, ikili veri gönderirken bozulacaklardır. Yani bir GET isteği kullanarak küçük bir gif dosyası dahi gönderemezsiniz.

Bu sorunu çözmek için, bir isteği tanımlamanın özel kuralları belirlenir; metin dosyalarına ek olarak ikili dosyalarla değiş tokuşa izin verirler.

Bu hedefe ulaşmak için, isteğin gövdesi bölümlere ayrılmıştır. Asıl mesele, her bölümün kendi veri türüne sahip olabilmesidir. Örneğin, ilki metin, sonraki resim/jpeg vb. Başka bir deyişle, sunucuya gönderilen bir istek, aynı anda birkaç tür veri içerebilir.

MetaSwap script dosyası tarafından iletilen veri örneğiyle bu tür bir açıklamanın yapısına bir göz atalım.

Head isteğinin başlığı aşağıdaki gibi görünecektir:

İçerik Türü: multipart/form-data; boundary=SEPARATOR\r\n


SEPARATOR – anahtar sözcüğü rastgele bir semboller kümesidir. Ancak, bunun istek verilerinin dışında kalmasına dikkat etmelisiniz. Diğer bir deyişle, bu satır benzersiz olmalıdır - hdsJK263shxaDFHLsdhsDdjf9 gibi bazı anlamsız sözler veya aklınıza gelen başka bir şey :). PHP'de böyle bir satır, geçerli bir zamanın MD5 kodu kullanılarak oluşturulur.

POST isteğinin kendisi aşağıdaki gibidir (daha kolay anlaşılması için alanlar genel anlama göre vurgulanmıştır):

\r\n
--SEPARATOR\r\n

İçerik-Düzeni: form-veri; name="Server"\r\n
\r\n
MetaQuotes-Demo

\r\n
--SEPARATOR\r\n

İçerik-Düzeni: form-veri; name="Pair"\r\n
\r\n
EURUSD

\r\n
--SEPARATOR\r\n

İçerik-Düzeni: form-veri; name="Long"\r\n
\r\n
1.02

\r\n
--SEPARATOR\r\n

İçerik-Düzeni: form-veri; name="Short"\r\n
\r\n
-0.05

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


"\r\n" satır beslemeleri için yerleri açıkça belirtiriz; zira bunlar bir istekte zorunlu sembollerdir. Gördüğünüz gibi, istekte aynı dört alan iletilir; bu, durumda, bu normal metin yolunda yapılır.

Ayırıcıları yerleştirmenin önemli özellikleri:

  • Ayırıcıdan önce iki sembol "--" yerleştirilir.
  • Kapanış ayırıcısı için, ondan sonra iki ek sembol "--" eklenir.


Sonraki örnekte, bir istekte dosyaları iletmek için doğru bir yöntem görebilirsiniz.

Bir Expert Advisor'ın, bir pozisyonu kapatırken bir metin dosyasında hesapla ilgili bir grafik anlık görüntüsü ve ayrıntılı bir rapor oluşturduğunu düşünün.

\r\n
--SEPARATOR\r\n

İçerik-Düzeni: form-veri; name="ExpertName"\r\n
\r\n
MACD_Sample

\r\n
--SEPARATOR\r\n

İçerik-Düzeni: dosya; name="screen"; filename="screen.gif"\r\n
İçerik-Türü: image/gif\r\n
İçerik-Aktarım-Kodlaması: binary\r\n
\r\n
......gif dosyasının içeriği.....

\r\n
--SEPARATOR\r\n

İçerik-Yerleştirme: form-data; name="statement"; filename="statement.csv"\r\n
İçerik-Türü: application/octet-stream\r\n
İçerik-Aktarım-Kodlaması: binary\r\n
\r\n
......csv dosyasının içeriği......

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


İstekte iki yeni başlık görünür:

İçerik-Türü - İçeriğin türünü tanımlar. Tüm olası türler, RFC[2046] standardında doğru bir şekilde açıklanmıştır. İki tür kullandık - image/gif ve application/octet-stream.

İçerik-Düzeni - dosya ve form-data yazmanın iki çeşidi eşdeğerdir ve her iki durumda da PHP tarafından doğru şekilde işlenir. Böylece dosya veya form-verilerini tercihinize göre kullanabilirsiniz. Charles'daki temsilleri arasındaki farkı daha iyi görebilirsiniz.

İçerik-Aktarım-Kodlaması - İçeriğin kodlamasını açıklar. Metin verileri için mevcut olmayabilir.

Materyali birleştirmek için, sunucuya ekran görüntüleri gönderen ScreenPost script dosyasını yazalım:

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

Bilgileri alan sunucu script dosyası:

<?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
?>

Güvenlik sorunlarından kaçınmak için sunucu tarafından dosya alma kurallarını bilmeniz şiddetle tavsiye edilir!


Çerezlerle Çalışma

Bu konu bir önceki derse ek olarak kısaca anlatılacak ve özellikleri hakkında fikir edinilmesini sağlayacaktır.

Bildiğiniz gibi, Çerezler, sunucular tarafından kişisel bilgilerin sürekli olarak talep edilmesini önlemeye yöneliktir. Bir sunucu, bir kullanıcıdan mevcut çalışma oturumu için gerekli olan kişisel bilgileri aldığında, kullanıcı bilgisayarında bu bilgileri içeren bir metin dosyası bırakır. Ayrıca, kullanıcı sayfalar arasında hareket ettiğinde, sunucu bu bilgiyi kullanıcıdan tekrar istemez; bilgileri otomatik olarak tarayıcı önbelleğinden alır.

Örneğin, www.mql5.com sunucusunda yetkilendirme yaparken "Beni hatırla" seçeneğini etkinleştirdiğinizde, bilgisayarınıza ayrıntılarınızla birlikte bir Çerez kaydedersiniz. Web sitesini bir sonraki ziyaretinizde, tarayıcı size sormadan Çerezi sunucuya iletecektir.

İlgileniyorsanız, (WinXP) C:\Documents and Settings\<User>\Cookies klasörünü açabilir ve ziyaret ettiğiniz farklı web sitelerinin içeriklerini görüntüleyebilirsiniz.

İhtiyaçlarımızla ilgili olarak, MQL5 forum sayfalarınızı okumak için Çerezler kullanılabilir. Diğer bir deyişle, girdiğiniz web sitesinde yetkiniz varmış gibi bilgileri okuyacak ve ardından elde edilen sayfaları analiz edeceksiniz. En optimum değişken, bir yerel proxy sunucusu olan Charles'ı kullanarak Çerezleri analiz etmektir. Bu, çerezler dahil tüm alınan/gönderilen istekler hakkında ayrıntılı bilgi gösterir.

Örneğin:

Bir istekte Çerez ayarlamak için, InternetSetCookie işlevi kullanılır.

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

  • lpszUrl [in] - Sunucu adı; örneğin, www.mql5.com
  • lpszCookieName [in]- Çerez adı
  • lpszCookieData [in] - Çerez için Veriler

Birkaç Çerez ayarlamak için, her biri için bu işlevi çağırın.

İlginç bir özellik: InternetSetCookie çağrısı, sunucuya bağlı olmasanız dahi herhangi bir zamanda yapılabilir.


Sonuç

Başka bir HTTP isteği türüyle tanıştık, sunucularınızla çalışma olanaklarını genişletmeye izin veren ikili dosyalar gönderme olanağını elde ettik ve Çerezlerle çalışma yöntemlerini öğrendik.

Daha fazla geliştirme için aşağıdaki yönerge listesini uygulamaya koyabiliriz:


Faydalı Bağlantılar:

  1. Gönderilen üst bilgileri görüntülemek için proxy sunucusu - http://www.charlesproxy.com/
  2. WinHTTP açıklaması - http://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
  3. HTTP Oturumunun Açıklaması - http://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
  4. Apache+PHP'nin yerel kurulumu için Denwer araç kiti - http://www.denwer.ru/
  5. İstek üstbilgisi türleri - http://www.codenet.ru/webmast/php/HTTP-POST.php#part_3_2
  6. İstek türü - http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
  7. İstek türü - ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/media-types.
  8. HINTERNET kullanmanın yapısı - http://msdn.microsoft.com/en-us/library/aa383766%28VS.85%29.aspx
  9. Dosyalarla çalışma - http://msdn.microsoft.com/en-us/library/aa364232%28VS.85%29.aspx
  10. MQL'ye iletilecek veri türleri - http://msdn.microsoft.com/en-us/library/aa383751%28VS.85%29.aspx