インターネットを介して端末間でデータ交換をするためのWinInet.dll利用

--- | 28 10月, 2015

蓄積されている数多くの新しいユーザーインターフェース要素を用いて、MetaTrader 5 はユーザーにまたとないチャンスの扉を開きます。これにより以前には利用できなかった関数がいまや最大限に活用できるのです。

このレッスンで学習することは以下です。

MQL5 CodeBase にはスクリプトの例が含まれています。それは wininet.dll ライブラリと連動し、サーバーページリクエストの例を表示します。ただ、ここではもっと先まで進み、ページをもたらすだけでなく、リクエストをする他の端末に対して後の転送用データを送信し格納 するサーバー作成まで行います。

注意:PHPで構成されているためサーバーにアクセスできない方は Denwer キットをダウンロードし、それをワーキング プラットフォームとして使用するとよいでしょう。また、検証用にはローカルホストで Apache サーバーおよび PHP の使用を推奨します。

サーバーにいずれのリクエストを送信するにも、ライブラリの主要関数7個が必要となります。

InternetAttemptConnect  インターネット接続セットと接続成立を試みます。
InternetOpen
WinInet ライブラリ関数の動作へのストラクチャを初期化します。この関数はライブラリのその他あらゆる関数を起動する前に起動する必要があります。
InternetConnect HTTP URL または FTPのアドレスによって指定されたリソースを開きます。オープン接続に記述を返します。
HttpOpenRequest 接続設定用 HTTP リクエストに対して記述を作成します。
HttpSendRequest 作成されたディスクリプタを使用してクエリーを送信します。
InternetReadFile クエリーが処理されたあとサーバーから受け取られたデータを読みます。
InternetCloseHandle 転送されたディスクリプタを解放します。

 
関数の詳述およびパラメータは MSDN ヘルプシステムにあります。

関数の宣言は MQL4でのものと同様ですが、ユニコード呼び出しとリンクによる行変換は異なります。

#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

フラグの詳述およびパラメータは MSDN の関数詳述と同じ項にあります。その他定数および関数の宣言を確認したければ、本稿添付の原ファイル wininet.h をダウンロードすることができます。

1. インターネットセッションの作成および削除に関するガイド

まず最初にすることはセッションを作成しホストへの接続をオープンすることです。セッションはプログラム初期化中には一度だけ作成することが望ましいとされています(たとえば OnInit 関数内で)。または Expert Advisor 起動の一番最初に行うことも可能ですが、重要なことはセッションのクローズ前に一度だけ問題なくかならずそれを作成することです。また、 OnStart OnTimer を実装するループで毎回繰り返し行ったり不必要に行うことはしません。頻繁な呼び出しや各呼び出しに必要なストラクチャを作成することを避けるのが重要です。

そのため、セッションの記述と接続ディスクリプタに対してはグローバルクラスインスタンスを一度だけ使用します。

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

初期化後、ディスクリプタ Session  および Connect は続く関数すべてで使用可能です。作業がすべて完了し、MQL プログラムがアンインストールされたら、それらは消去する必要があります。これは InternetCloseHandle 関数を用いて行います。

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

注意! インターネット関数に連動する際は、InternetCloseHandle を用いてそれらによって駆動されるディスクリプタをすべて解放する必要があります。

2. サーバーに対するリクエスト送信およびページ受け取り

リクエストを送信し、そのリクエストに対する回答を受け取るには関数があと3つ必要です。それらは、HttpOpenRequestHttpSendRequest InternetReadFileです。リクエストに対する回答ページを受け取る最重要点は基本的にそのコンテンツをローカルファイルに保存するシンプルな手続きです。


リクエストとコンテンツの作業がしやすいように汎用関数を2つ作成します。

リクエスト送信

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

MqlNet:: Request の関数パラメータ

受け取られるディスクリプタのコンテンツ読み出し

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

MqlNet:: ReadPage関数パラメータ

これらをすべて一つにまとめると、インターネットと連携する MqlNet ライブラリクラスを取得します。

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

それは基本的にインターネットと連携するための多様なニーズを満たすであろう必要なすべての関数です。その使用例を考察します。

例1 MQLプログラムの端末フォルダへの 自動ダウンロードMetaGrabber スクリプト

クラス動作検証を始めるにあたりもっとも簡単なタスクから始めます。それはページを読み、そのコンテンツを指定フォルダーに保存することです。ただしページをただ単に読むことはあまり面白いものではなさそうです。よってスクリプトの動作から何かを得るために、そこにサイトからの mql プログラムのグラバー機能を割り当てます。MetaGrabber スクリプトのタスクは以下です。

二番目の問題解決には MqlNet クラスを使用します。三番目のタスクには Kernel32.dllからの関 数MoveFileEx を使用します。

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

最初の課題に対して URL 行をパースする短いサービス関数を作成します。

アドレスから3行を割り当てる必要があります。それはホスト、サイトへのパス、ファイル名です。
たとえば行 http://www.mysite.com/folder/page.html では以下のように行います。

- ホスト = www.mysite.com
- リクエスト = / folder / page.html
- ファイル名 = page.html

MQL5 サイトの CodeBase の場合は、パスウェイ は同じストラクチャをしています。たとえばページ https://www.mql5.com/ru/code/79 looks like http://p.mql5.com/data/18/79/ErrorDescription.mqh のライブラリ ErrorDescription.mq5 へのパスです。このパスはリンクを右クリックし『リンクをコピーする』を選択するだけで簡単に取得されます。よってURL は2つに分割されます。一つはリクエスト用、もう一つはファイル格納をしやすくするファイル名用です。

- ホスト = p.mql5.com
- リクエスト = / data/18/79/5/ErrorDescription.mqh
- ファイル名 = ErrorDescription.mqh

これは次の関数 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;
  }

スクリプトの外部パラメータではパラメータを2つだけ作成します。 URL(mql5 ファイルのパス)とそれに続く配置のフォルダータイプです。これはパスをセットしたい端末フォルダです。

そして取得するのが、短いですがひじょうに有用な以下のようなスクリプトです。

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


お気に入りのセクション https://www.mql5.com/en/code について実験をします。ダウンロードされたファイルはすぐにエディタのナビゲータに表示されます。それらは端末またはエディタを再起動しなくてもコンパイル可能です。ファイルの移動先として望ましいフォルダを検索するファイルシステムの長いパスをさまようことはありません。

注意!数多くのサイトは大量のコンテントをダウンロードすることに対してセキュリティを設定しています。そのような大量ダウンロードの場合は、みなさんの IPアドレスがこのリソースによりブロックされる可能性があります。よって頻繁に利用し使用を禁じされたくないリソースからファイルの『マシン』ダウンロードを利用する際は最新の注意を払ってください。

提案のサービスを改良してさらに一歩踏み込みたい方は、クリップボードコンテンツのインターセプションとそれ以上の自動ダウンロードを持つ Clipboard スクリプトの利用が可能です。

例2 単一チャート上で複数仲介会社からのクオート監視

というわけでインターネットからファイルを取得する方法は学習しました。そこでもっと興味深い課題を考察します。サーバー上でのこのデータの送信および格納方法です。これを行うには、短い PHPスクリプトが必要です。それをサーバーにセットします。すでに書かれた MqlNet クラスを利用して監視 MetaArbitrage 用 Expert Advisor を作成します。PHPスクリプトと連動したエキスパートのタスクは次のようなものです。

MQLモジュールと PHPスクリプト間の連携についてのスキーム図を以下に示します。


これら課題を解決するために MqlNet クラスを使います。

データの重複を避けるため、また古いクオートを排除するため、おもなパラメータを4つ送信します。仲介会社のサーバー名(現在価格のソース)、通貨、価格、UTCにおけるクオート時刻です。たとえば、弊社のリソースからのスクリプトへのアクセス リクエストは以下のようなものです。q

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

これらパラメータと実際のクオートがサーバーに格納され、該当通貨のその他格納されたクオートすべてと共に回答ページで公表されます。

この交換の『付加的な』利便性はクオートが MT5 同様 MT4 からも送信することができることです!

サーバーによって作成されるページは通常の CSV ファイルです。このスクリプトではそれは以下のように記述されます。

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

ServerName N; BidN; TimeN

またご自身の別パラメータをそれに追加することも可能です(たとえばサーバータイプでデモまたは実際の)。この CSV ファイルを格納し、値のテーブルおよび画面に表示される価格行のアウトプットで一行ずつパースします。

このファイル処理は、それぞれ特殊なケースで要求されるものを選択し多くの異なる方法で行うことが可能です。たとえば MetaTrader 4 デモサーバーから受け取られたクオートを除外する、などです。


インターネットサーバーを利用するメリットは明白です。みなさんはご自身のクオートを送信し、それを別のトレーダーが受信し閲覧することができるのです。同様に他のトレーダーが送信したクオートをみなさんも受信するのです。それは、端末間相互交流が両側で行われる、データ交換が以下のスキームで示されるように行われる、ということです。


このスキームはあらゆる端末数間での情報交換の原則に対する基盤の役割を果たします。注釈付の完全な MetaArbitrage Expert Advisor と PHPスクリプトは本稿添付のリンクからダウンロードすることができます。PHP利用関数についてさらなる内容はサイ トphp.su で読むことができます。

例3 端末内でのメッセージ交換(ミニチャット)MetaChat Expert Advisor

トレーディングや数字から一歩離れ、端末を終了することなく同時に複数の人とチャットできるアプリケーションを作成します。このためにはもう一つ PHP スクリプトが必要です。それは通常前に作成したものとひじょうに似通ったものです。ただしこのスクリプトで時刻引用を分析する代わりにファイルの行数を分析する点は例外です。Expert Advisor のタスクは次のようなものです。

MetaChat の動作は以前の Expert Advisorのそれとは異なります。原理、シンプルなアウトプット用CSV ファイルは同じです。


MetaChat および MetaArbitrage 開発者のサイトに保管されています。それらの動作用PHPスクリプトも同じ場所に保管されています。
よって動作検証を行ったりこのサービスを利用したい場合は、以下のリンクからそこにアクセスできます。
MetaСhat - www.fxmaster.de/metachat.php
MetaArbitrage - www.fxmaster.de/metaarbitr.php

おわりに

これでHTTPリクエストに関してよく理解しました。インターネットを通じてデータ送受信をし、またいっそう快適に作業手順を組み立てる技能を得ました。しかしどんな機能もつねに改善の可能性があるものです。以下はそれら改善の新しい潜在的な改善方向と考えることができます。

本稿ではリクエストの GET タイプを使用しました。サーバー分析のために少数のパラメータを伴ってファイル取得、リクエスト送信が必要なとき、それは十分にタスクを達成します。

次のレッスンでは POST リクエストを細かく見ていきます。それはサーバーへのファイル送信または端末間でのファイル共有を行うものです。そしてその利用例を考察します。

有用な情報リソース