电竞比分网-中国电竞赛事及体育赛事平台

分享

自己動(dòng)手開(kāi)發(fā)多線程異步 MQL5 WebRequest

 Levy_X 2019-01-11

交易算法的實(shí)現(xiàn)經(jīng)常需要分析來(lái)自各種外部來(lái)源、包括互聯(lián)網(wǎng)的數(shù)據(jù),MQL5 提供了 WebRequest 函數(shù)來(lái)發(fā)送 HTTP 請(qǐng)求到 '外部世界', 然而不幸的是,它有一個(gè)明顯的缺點(diǎn)。這個(gè)函數(shù)是同步的,也就是說(shuō)它會(huì)在執(zhí)行請(qǐng)求的整個(gè)階段阻止 EA 的運(yùn)行。對(duì)于每個(gè) EA, MetaTrader 5 都為它們分配了一個(gè)單獨(dú)的線程,在代碼中執(zhí)行已有的 API(應(yīng)用程序接口) 函數(shù),以及執(zhí)行到來(lái)的事件處理函數(shù) (例如分時(shí),市場(chǎng)深度變化的事件,計(jì)時(shí)器,交易操作,圖表事件等等)。一次只執(zhí)行一個(gè)代碼片段,而所有剩余的“任務(wù)”都排隊(duì)等候,直到當(dāng)前片段把控制權(quán)還給內(nèi)核。

例如,如果一個(gè)EA交易要實(shí)時(shí)處理新的分時(shí),也要間斷檢查一個(gè)或者多個(gè)網(wǎng)站上的經(jīng)濟(jì)新聞,就不可能實(shí)現(xiàn)這兩個(gè)需求而使它們不互相影響。一旦 WebRequest 在代碼中執(zhí)行,EA 交易就會(huì)在函數(shù)調(diào)用序列中保持“凍結(jié)”狀態(tài),而新的分時(shí)事件就會(huì)跳過(guò)。就算可以使用 CopyTicks 函數(shù)可能可以讀取到跳過(guò)的分時(shí),而做出交易決策的時(shí)機(jī)已經(jīng)錯(cuò)過(guò)了。這里是使用UML順序圖表來(lái)闡述的這種情況:

事件處理序列圖是在一個(gè)線程中的阻塞式代碼

圖1. 事件處理序列圖是在一個(gè)線程中的阻塞式代碼

在這一點(diǎn)上,最好能創(chuàng)建一個(gè)工具用于 HTTP 請(qǐng)求的異步執(zhí)行,相當(dāng)于一種 WebRequestAsync,很明顯,我們需要為此使用額外的線程。在 MetaTrader 5 中這樣做的最簡(jiǎn)單方法就是運(yùn)行另外的EA,然后您可以在其中發(fā)送另外的 HTTP 請(qǐng)求。另外, 您可以在那里調(diào)用 WebRequest 并隨后取得結(jié)果,當(dāng)請(qǐng)求在這樣的輔助EA交易中處理的時(shí)候,我們的主EA交易還是可以用于快速和交互的操作。對(duì)于這種情況,UML 順序圖表可能看起來(lái)是這樣的:

向其它線程分發(fā)異步事件處理的順序圖表

圖 2. 向其它線程分發(fā)異步事件處理的順序圖表


1. 計(jì)劃

您應(yīng)該知道,在MetaTrader中,每個(gè)EA交易都運(yùn)行在獨(dú)立的圖表中。因而,創(chuàng)建輔助EA交易也需要它們的圖表,人工來(lái)做很不方便,所以,把所有的常規(guī)操作放到一個(gè)特定的管理器中就很合理了 - 一個(gè)管理輔助圖表和EA池的EA交易,并且它提供了一個(gè)入口點(diǎn)用于注冊(cè)來(lái)自客戶(hù)程序的新請(qǐng)求。在某種程度上,這種架構(gòu)可以被稱(chēng)為三層架構(gòu),與客戶(hù)端-服務(wù)器架構(gòu)類(lèi)似,其中EA管理器作為服務(wù)器:

multiweb 開(kāi)發(fā)庫(kù)的架構(gòu): 客戶(hù) MQL 代碼 - 服務(wù)器 (助理池管理器) - 助手 EA

圖 3 multiweb 開(kāi)發(fā)庫(kù)的架構(gòu): 客戶(hù) MQL 代碼 <-> 服務(wù)器 (助理池管理器) <-> 助手 EA

但是為了簡(jiǎn)化,管理器和輔助EA可以使用相同的代碼樣式來(lái)實(shí)現(xiàn) (程序)。這樣一個(gè) '通用' EA 的兩種角色之一 - 管理器或是助手 - 將會(huì)由優(yōu)先級(jí)規(guī)則來(lái)判斷。第一個(gè)運(yùn)行的實(shí)例會(huì)把它自己聲明為一個(gè)管理器,它可以打開(kāi)另外的圖表并以助手的角色運(yùn)行一定數(shù)量的自身。

那么在客戶(hù)端、管理器和助手之間到底是怎樣傳遞信息的呢?為了了解這個(gè),讓我們分析 WebRequest 函數(shù),

您要知道,MetaTrader 5 含有兩種 WebRequest 函數(shù)的選項(xiàng),我們將會(huì)考慮使用第二種,它是最通用的。

int WebRequest (   const string      method,           // HTTP 方法   const string      url,              // url 地址   const string      headers,          // 請(qǐng)求頭部     int               timeout,          // 超時(shí)   const char        &data[],          // HTTP 消息體數(shù)組   char              &result[],        // 服務(wù)器回復(fù)數(shù)據(jù)的數(shù)組   string            &result_headers   // 服務(wù)器回復(fù)的頭部 );

前面五個(gè)參數(shù)是輸入?yún)?shù),它們是從調(diào)用代碼傳遞到核心的,定義了請(qǐng)求的內(nèi)容。后面兩個(gè)參數(shù)是輸出參數(shù),它們是從核心傳遞到調(diào)用代碼,包含了查詢(xún)的結(jié)果。很明顯,把這個(gè)函數(shù)變成兩個(gè)異步函數(shù)實(shí)際上要把它分成兩個(gè)組件:初始化查詢(xún)和取得結(jié)果:

int WebRequestAsync
( 
  const string      method,           // HTTP 方法 
  const string      url,              // url 地址 
  const string      headers,          // 請(qǐng)求頭部  
  int               timeout,          // 超時(shí) 
  const char        &data[],          // HTTP 消息體數(shù)組 
);

int WebRequestAsyncResult
( 
  char              &result[],        // 服務(wù)器回復(fù)數(shù)據(jù)的數(shù)組 
  string            &result_headers   // 服務(wù)器回復(fù)的頭部 
);

函數(shù)的名稱(chēng)和原型是由條件的,實(shí)際上,我們需要在不同的MQL程序之間傳遞這個(gè)信息,普通的函數(shù)調(diào)用對(duì)此并不適合。為了使 MQL 程序相互之間能夠“聯(lián)絡(luò)”,MetaTrader 5 有我們將要使用的 自定義事件交換系統(tǒng)。事件的交換是根據(jù)接收者ID 進(jìn)行的,使用的是 ChartID — 它對(duì)每個(gè)圖表都是唯一的。一張圖表上只允許有一個(gè)EA交易,但是指標(biāo)的話使用起來(lái)沒(méi)有限制。這意味著用戶(hù)應(yīng)當(dāng)確保每個(gè)圖表中與管理器通信的指標(biāo)不能超過(guò)一個(gè)。

為了使數(shù)據(jù)交換可行,您需要把所有的“函數(shù)”參數(shù)都封裝到用戶(hù)事件參數(shù)中。請(qǐng)求參數(shù)和結(jié)果都包含了比較大量的信息,而無(wú)法容納在事件有限的空間中,例如,就算要把 HTTP 方法和URL放到字符串類(lèi)型的事件參數(shù)中,63個(gè)字符的限制在大多數(shù)實(shí)際情況下都有困難。這就意味著事件交換系統(tǒng)需要有一些共享數(shù)據(jù)的存放空間,而只在事件參數(shù)中發(fā)送這個(gè)存放空間的鏈接。幸運(yùn)的是,MetaTrader 5 以自定義資源的形式提供了這樣的存儲(chǔ)。實(shí)際上,從MQL中動(dòng)態(tài)創(chuàng)建的資源都是圖片,但是圖片可以作為二進(jìn)制信息的容器,我們可以在里面記錄我們想要的任何內(nèi)容。

為了簡(jiǎn)化任務(wù),我們將會(huì)使用已有的方案來(lái)在用戶(hù)資源中寫(xiě)入和讀取數(shù)據(jù) — 來(lái)自 MQL5 社區(qū)成員 fxsaber 所開(kāi)發(fā)的 Resource.mqh and ResourceData.mqh。

所提供的鏈接指向的源代碼 — TradeTransactions 開(kāi)發(fā)庫(kù)與當(dāng)前文章的主題沒(méi)有關(guān)系,但是在討論 (俄語(yǔ)) 中包含了一個(gè)通過(guò)使用資源來(lái)作數(shù)據(jù)存儲(chǔ)和交換的例子。因?yàn)殚_(kāi)發(fā)庫(kù)可以修改,另外為了方便讀者使用,所有本文使用的文件都在下面的附件中,但是它們的版本只是對(duì)應(yīng)著寫(xiě)這篇文章的時(shí)間,可能與以上鏈接中的當(dāng)前版本不一樣。另外,所說(shuō)的資源類(lèi)在它們的工作中還使用了另一個(gè)開(kāi)發(fā)庫(kù) — TypeToBytes,它的版本也附加在本文之后了。

我們不需要關(guān)心這些輔助類(lèi)的內(nèi)部結(jié)構(gòu),主要的事情就是我們可以把做好的 RESOURCEDATA 類(lèi)當(dāng)作 “黑盒”,再使用它的構(gòu)造函數(shù)和我們需要的一些方法,晚些時(shí)候我們將詳細(xì)探討?,F(xiàn)在,讓我們看看整體的概念。

我們的架構(gòu)中交互的順序看起來(lái)如下:

  1. 為了進(jìn)行異步的 web 請(qǐng)求,客戶(hù) MQL 程序應(yīng)當(dāng)使用我們開(kāi)發(fā)的類(lèi)來(lái)把請(qǐng)求的參數(shù)封裝到本地資源中,并向管理器使用資源的連接發(fā)送一個(gè)自定義事件,資源是在客戶(hù)程序中創(chuàng)建的,直到取得結(jié)果之前都不會(huì)被刪除(當(dāng)不需要時(shí)刪除);
  2. 管理器在池中尋找未被占用的助手EA并向其發(fā)送資源的連接;這個(gè)實(shí)例就會(huì)暫時(shí)標(biāo)記為被占用而在隨后的請(qǐng)求中不能被選擇,直到當(dāng)前請(qǐng)求被處理完畢;
  3. 當(dāng)輔助EA接收到自定義事件的時(shí)候,來(lái)自客戶(hù)的web請(qǐng)求參數(shù)就從外部資源中解開(kāi),
  4. 輔助EA再調(diào)用標(biāo)準(zhǔn)的阻塞式 WebRequest 并等待回答(頭部以及/或者web文檔);
  5. 輔助EA再把請(qǐng)求的結(jié)果打包到本地資源中,并向管理器發(fā)送包含這個(gè)連接的自定義事件;
  6. 管理器把事件轉(zhuǎn)發(fā)到客戶(hù)并把對(duì)應(yīng)的輔助EA再次標(biāo)記為空閑;
  7. 客戶(hù)端收到來(lái)自管理器的信息并從外部輔助資源中解開(kāi)請(qǐng)求的結(jié)果;
  8. 然后客戶(hù)端和輔助EA可以刪除它們的本地資源了。

在第5步和第6步結(jié)果效率可以進(jìn)一步提高,因?yàn)檩o助EA可以直接向客戶(hù)窗口發(fā)送結(jié)果而繞過(guò)管理器。

上面所描述的步驟與處理HTTP請(qǐng)求的主要階段有關(guān),現(xiàn)在,是時(shí)候談一下如何把分離的部分連接成一個(gè)整體架構(gòu)了,它也是部分依賴(lài)于用戶(hù)事件。

架構(gòu)的中心連接 — 管理器 — 是人工運(yùn)行的,您應(yīng)當(dāng)只要做一次。和任何其它運(yùn)行的EA一樣,在終端重新啟動(dòng)時(shí)它會(huì)與圖表一起自動(dòng)恢復(fù),終端中只允許有一個(gè) web 請(qǐng)求管理器。

管理器會(huì)創(chuàng)建所要求數(shù)量的輔助窗口 (可以在設(shè)置中設(shè)定) 并在它們中運(yùn)行它自身的實(shí)例,它們可以通過(guò)特殊的“協(xié)議”發(fā)現(xiàn)它們自己是助手狀態(tài) (在實(shí)現(xiàn)部分有詳細(xì)介紹)。

任何助手在關(guān)閉時(shí)都會(huì)通過(guò)特定事件來(lái)通知管理器,這對(duì)管理器維護(hù)相關(guān)可用的助手列表是必需的。類(lèi)似地,管理器關(guān)閉時(shí)也可以通知助手,然后,助手就會(huì)停止工作并關(guān)閉它們的窗口。助手離開(kāi)管理器是無(wú)法工作的,而重新運(yùn)行管理器會(huì)不可避免地重新創(chuàng)建助手 (例如,如果您在設(shè)置中修改了助手的數(shù)量)。

助手的窗口,和輔助EA自身類(lèi)似,總是由管理器自動(dòng)創(chuàng)建的,所以我們的程序應(yīng)當(dāng)“清除它們”。不要人工運(yùn)行助手EA - 輸入?yún)?shù)與管理器狀態(tài)不對(duì)應(yīng)的話會(huì)被程序當(dāng)成錯(cuò)誤。

在它載入的時(shí)候,客戶(hù) MQL 程序應(yīng)當(dāng)偵測(cè)終端窗口看管理器是否存在,它是把它的圖表ID作為參數(shù)然后使用消息來(lái)探查的。管理器 (如果被發(fā)現(xiàn)) 應(yīng)當(dāng)把它的窗口ID返回給客戶(hù),之后,客戶(hù)和管理器就能交換消息了。

這些就是主要功能,是時(shí)候開(kāi)始進(jìn)行實(shí)現(xiàn)了。


2. 實(shí)現(xiàn)

為了簡(jiǎn)化開(kāi)發(fā),創(chuàng)建一個(gè) multiweb.mqh 頭文件,我們?cè)谄渲新暶魉械念?lèi): 它們中的一些對(duì)客戶(hù)端和 '服務(wù)器' 都是通用的, 而其它一些是繼承的而分別針對(duì)這些角色。

2.1. 基類(lèi) (開(kāi)始)

讓我們從保存資源、ID和每個(gè)元素變量的類(lèi)開(kāi)始。從它派生出的類(lèi)的實(shí)例將可以用在管理器、助手和客戶(hù)中。在客戶(hù)和助手中,這樣的對(duì)象主要是用于保存“通過(guò)連接傳遞的“資源,另外,要注意的是在客戶(hù)端中要?jiǎng)?chuàng)建幾個(gè)實(shí)例來(lái)同時(shí)執(zhí)行多個(gè)web請(qǐng)求。所以,分析當(dāng)前請(qǐng)求的狀態(tài) (至少是這樣的對(duì)象是繁忙還是空閑) 應(yīng)當(dāng)在客戶(hù)中廣泛被使用。在管理器中,這些對(duì)象是用于實(shí)現(xiàn)助手狀態(tài)的識(shí)別和跟蹤的。下面就是基類(lèi)。

class WebWorker {   protected:     long chartID;     bool busy;     const RESOURCEDATA<uchar> *resource;     const string prefix;          const RESOURCEDATA<uchar> *allocate()     {       release();       resource = new RESOURCEDATA<uchar>(prefix (string)chartID);       return resource;     }        public:     WebWorker(const long id, const string p = 'WRP_'): chartID(id), busy(false), resource(NULL), prefix('::' p)     {     }     ~WebWorker()     {       release();     }          long getChartID() const     {       return chartID;     }          bool isBusy() const     {       return busy;     }          string getFullName() const     {       return StringSubstr(MQLInfoString(MQL_PROGRAM_PATH), StringLen(TerminalInfoString(TERMINAL_PATH)) 5) prefix (string)chartID;     }          virtual void release()     {       busy = false;       if(CheckPointer(resource) == POINTER_DYNAMIC) delete resource;       resource = NULL;     }     static void broadcastEvent(ushort msg, long lparam = 0, double dparam = 0.0, string sparam = NULL)     {       long currChart = ChartFirst();       while(currChart != -1)       {         if(currChart != ChartID())         {           EventChartCustom(currChart, msg, lparam, dparam, sparam);         }         currChart = ChartNext(currChart);       }     } };

變量描述:

  • chartID — MQL 程序所運(yùn)行的圖表的 ID;
  • busy — 當(dāng)前實(shí)例是否正在忙于處理 web請(qǐng)求;
  • resource — 對(duì)象的資源 (隨機(jī)數(shù)據(jù)存儲(chǔ)); RESOURCEDATA 類(lèi)來(lái)自于 ResourceData.mqh;
  • prefix — 對(duì)于每個(gè)狀態(tài)的唯一前綴; 前綴是用于資源名稱(chēng)的。在特定的客戶(hù)中,推薦按照下面的例子來(lái)進(jìn)行獨(dú)特設(shè)置。助手 EA 默認(rèn)使用的是 'WRR_' (Web Request Result 的簡(jiǎn)寫(xiě)) 前綴。

'allocate' 方法是在派生類(lèi)中使用的,它在 ’resource' 變量中創(chuàng)建 RESOURCEDATA<uchar> 類(lèi)型的資源對(duì)象,chart ID 也用于和前綴一起命名資源。資源可以使用 'release' 方法來(lái)釋放。

應(yīng)當(dāng)特別提到 getFullName 方法,因?yàn)樗祷赝暾馁Y源名稱(chēng),包含了當(dāng)前 MQL 程序名稱(chēng)和目錄的路徑,完整名稱(chēng)用于訪問(wèn)第三方程序的資源 (是只讀的)。例如,如果 multiweb.mq5 EA 位于 MQL5\Experts 并且在 ID 為 129912254742671346 的圖表中載入, 其中資源的完整名稱(chēng)就是 '\Experts\multiweb.ex5::WRR_129912254742671346',我們將把這樣的字符串作為資源連接,在自定義事件的 sparam 字符串型參數(shù)中使用。

broadcastEvent 靜態(tài)方法可以向所有窗口發(fā)信息,在將來(lái)用于尋找管理器。

為了在客戶(hù)程序中使用請(qǐng)求和相關(guān)的資源,我們定義了 ClientWebWorker 類(lèi),它派生于 WebWorker (這里的代碼省略了,完整版在附件的文件中)。

class ClientWebWorker : public WebWorker
{
  protected:
    string _method;
    string _url;
    
  public:
    ClientWebWorker(const long id, const string p = 'WRP_'): WebWorker(id, p)
    {
    }

    string getMethod() const
    {
      return _method;
    }

    string getURL() const
    {
      return _url;
    }
    
    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      // allocate()? and what's next?
      ...
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ': Reading result ', resname);
      
      ...
    }
};

首先,請(qǐng)注意 'request' 方法也就是實(shí)現(xiàn)了上面所述的第一步,也就是把 web 請(qǐng)求發(fā)送到管理器。這個(gè)方法的聲明是跟隨假定的 WebRequestAsync 原型的,而 receiveResult 靜態(tài)方法進(jìn)行的就是第七步所說(shuō)的反向過(guò)程。對(duì)于第一個(gè)輸入?yún)?shù) 'resname', 它是請(qǐng)求結(jié)果中保存的外部資源的完整名稱(chēng),而 'initiator', 'headers' 和 'text' 字節(jié)數(shù)組是方法中從資源中解開(kāi)數(shù)據(jù)之后填充的。

什么是 'initiator'?答案很簡(jiǎn)單,因?yàn)槲覀兯械?'調(diào)用' 現(xiàn)在都是異步的 (而它們的執(zhí)行順序都不保證), 我們應(yīng)當(dāng)能夠把結(jié)果和之前發(fā)送的請(qǐng)求匹配起來(lái)。所以,助手 EA 要把來(lái)源客戶(hù)資源的完整名稱(chēng)打包,用于初始化請(qǐng)求到回應(yīng)資源中,與從互聯(lián)網(wǎng)中取得的數(shù)據(jù)放在一起。在解包之后,'initiator' 參數(shù)中的名稱(chēng)可以用于把結(jié)果與對(duì)應(yīng)的請(qǐng)求關(guān)聯(lián)起來(lái)。

receiveResult 方法是靜態(tài)的,因?yàn)樗鼪](méi)有使用對(duì)象的變量 - 所有從調(diào)用代碼中返回的結(jié)果都是通過(guò)參數(shù)傳遞的。

這兩個(gè)方法在從資源中打包和解包數(shù)據(jù)都是需要的,這將在下一個(gè)部分中介紹。


2.2. 把請(qǐng)求和請(qǐng)求結(jié)果打包進(jìn)資源

我們應(yīng)當(dāng)記得,在底層水平,資源是通過(guò) RESOURCEDATA 類(lèi)來(lái)處理的,這是一個(gè)模板(template)類(lèi), 也就是說(shuō)它可以接收含有數(shù)據(jù)類(lèi)型的參數(shù), 我們可以寫(xiě)入資源或者從資源中讀取。因?yàn)槲覀兊臄?shù)據(jù)也包含字符串,有理由選擇最小的 uchar 類(lèi)型作為存儲(chǔ)單位,這樣,我們就把 RESOURCEDATA<uchar> 類(lèi)的對(duì)象用作數(shù)據(jù)容器。當(dāng)創(chuàng)建資源時(shí),要在它的構(gòu)造函數(shù)中創(chuàng)建一個(gè)唯一(對(duì)于程序來(lái)說(shuō))的 'name'。

RESOURCEDATA<uchar>(const string name)

我們可以把這個(gè)名稱(chēng) (加上程序名稱(chēng)作為前綴) 傳給自定義事件,這樣其它 MQL 程序就能訪問(wèn)同樣的資源了。請(qǐng)注意,所有其它程序,除了創(chuàng)建資源的之外,都是有只讀的訪問(wèn)權(quán)限。

數(shù)據(jù)寫(xiě)入資源使用的是重載過(guò)的賦值操作符:

void operator=(const uchar &array[]) const

其中'array' 是我們需要準(zhǔn)備的一種數(shù)組。

從資源中讀取數(shù)據(jù)使用的是這個(gè)函數(shù):

int Get(uchar &array[]) const

在此,'array' 是一個(gè)輸出參數(shù),也包含最初數(shù)組的內(nèi)容。

現(xiàn)在讓我們轉(zhuǎn)到應(yīng)用部分,就是使用資源來(lái)傳遞關(guān)于 HTTP 請(qǐng)求和它們結(jié)果的數(shù)據(jù)。我們將會(huì)在資源和主代碼之間創(chuàng)建一個(gè)層次類(lèi) - ResourceMediator. 這個(gè)類(lèi)用于把 'method', 'url', 'headers', 'timeout' 和 'data' 參數(shù)打包到 'array' 類(lèi)型的數(shù)組,然后再在客戶(hù)端寫(xiě)到資源中。在服務(wù)器端,就要從資源中解包參數(shù)。類(lèi)似地,這個(gè)類(lèi)也要把服務(wù)器端的 'result' 和 'result_headers' 參數(shù)打包到 'array' 字節(jié)數(shù)組中并寫(xiě)到資源里,然后在客戶(hù)端可以把它們以數(shù)組形式讀出并解包。

ResourceMediator 構(gòu)造函數(shù)接收 RESOURCEDATA 資源的指針,它會(huì)在方法內(nèi)部進(jìn)行處理。另外,ResourceMediator 包含了用于保存有關(guān)數(shù)據(jù)的元信息的結(jié)構(gòu)。實(shí)際上,當(dāng)打包和解包資源時(shí),我們需要某種頭部信息包含所有欄位以及數(shù)據(jù)本身的大小。

例如,如果我們只是簡(jiǎn)單使用 StringToCharArray 函數(shù)把一個(gè) URL 轉(zhuǎn)換為一個(gè)字節(jié)數(shù)組,那么當(dāng)進(jìn)行反向操作的時(shí)候,使用 CharArrayToString 我們需要設(shè)置數(shù)組的長(zhǎng)度,否則,不僅 URL 字節(jié)本身,之后的頭部欄位都會(huì)被從數(shù)組中讀取出來(lái)。您也許記得,在寫(xiě)入資源之前,我們把所有數(shù)據(jù)都寫(xiě)到一個(gè)單獨(dú)的數(shù)組中了。關(guān)于欄位長(zhǎng)度的元信息也應(yīng)當(dāng)被轉(zhuǎn)換為字節(jié)的序列,為此我們使用的是聯(lián)合(union)。

#define LEADSIZE (sizeof(int)*5) // web-request 中的5個(gè)欄位

class ResourceMediator
{
  private:
    const RESOURCEDATA<uchar> *resource; // underlying asset
    
    // 頭部的元數(shù)據(jù)可以是5個(gè)整數(shù)的‘lengths’或者是一個(gè)字節(jié)數(shù)組`sizes`
    union lead
    {
      struct _l
      {
        int m; // 方法
        int u; // url
        int h; // 頭部
        int t; // 超時(shí)
        int b; // 主體
      }
      lengths;
      
      uchar sizes[LEADSIZE];
      
      int total()
      {
        return lengths.m   lengths.u   lengths.h   lengths.t   lengths.b;
      }
    }
    metadata;
  
    // 整數(shù)和字節(jié)數(shù)組
    union _s
    {
      int x;
      uchar b[sizeof(int)];
    }
    int2chars;
    
    
  public:
    ResourceMediator(const RESOURCEDATA<uchar> *r): resource(r)
    {
    }
    
    void packRequest(const string method, const string url, const string headers, const int timeout, const uchar &body[])
    {
      // 使用參數(shù)數(shù)據(jù)長(zhǎng)度填充元數(shù)據(jù)
      metadata.lengths.m = StringLen(method)   1;
      metadata.lengths.u = StringLen(url)   1;
      metadata.lengths.h = StringLen(headers)   1;
      metadata.lengths.t = sizeof(int);
      metadata.lengths.b = ArraySize(body);
      
      // 分配結(jié)果數(shù)組,用于元數(shù)據(jù)和參數(shù)數(shù)據(jù)
      uchar data[];
      ArrayResize(data, LEADSIZE   metadata.total());
      
      // 把元數(shù)據(jù)以字節(jié)數(shù)組方式放在數(shù)組的開(kāi)始
      ArrayCopy(data, metadata.sizes);
      
      // 把數(shù)據(jù)欄位挨個(gè)放到數(shù)組中
      int cursor = LEADSIZE;
      uchar temp[];
      StringToCharArray(method, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor  = metadata.lengths.m;
      
      StringToCharArray(url, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor  = metadata.lengths.u;
      
      StringToCharArray(headers, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor  = metadata.lengths.h;
      
      int2chars.x = timeout;
      ArrayCopy(data, int2chars.b, cursor);
      cursor  = metadata.lengths.t;
      
      ArrayCopy(data, body, cursor);
      
      // 在資源中存儲(chǔ)數(shù)組
      resource = data;
    }
    
    ...

首先,packRequest 方法把所有欄位的大小寫(xiě)到 'metadata' 結(jié)構(gòu)中,然后這個(gè)結(jié)構(gòu)的內(nèi)容被以字節(jié)數(shù)組的形式復(fù)制到‘data'數(shù)組的開(kāi)始部分。'data' 數(shù)組隨后會(huì)被放到資源中。'data' 數(shù)組的大小是根據(jù)所有欄位的總長(zhǎng)度以及元數(shù)據(jù)結(jié)構(gòu)的大小保留的。字符串類(lèi)型的參數(shù)使用 StringToCharArray 轉(zhuǎn)為數(shù)組并復(fù)制到結(jié)果數(shù)組中,還要有對(duì)應(yīng)的偏移,偏移是在’cursor'變量中保存的。'timeout' 參數(shù)使用int2chars聯(lián)合而被轉(zhuǎn)換為字符數(shù)組。'body' 參數(shù)就按原樣復(fù)制到數(shù)組中,因?yàn)樗呀?jīng)是所需類(lèi)型的數(shù)組了。最后,把通用數(shù)組中的內(nèi)容依次移動(dòng)到資源中 (您也許記得, 在 RESOURCEDATA 類(lèi)中的 ‘=' 操作符被重載過(guò)了):

      resource = data;

在 unpackRequest 方法中對(duì)資源中參數(shù)的讀取所進(jìn)行的是反向的操作。

    void unpackRequest(string &method, string &url, string &headers, int &timeout, uchar &body[])
    {
      uchar array[];
      // 使用資源中的數(shù)據(jù)填充數(shù)組  
      int n = resource.Get(array);
      Print(ChartID(), ': Got ', n, ' bytes in request');
      
      // 從數(shù)組中讀取元數(shù)據(jù)
      ArrayCopy(metadata.sizes, array, 0, 0, LEADSIZE);
      int cursor = LEADSIZE;

      // 一個(gè)個(gè)讀取所有的數(shù)據(jù)欄位      
      method = CharArrayToString(array, cursor, metadata.lengths.m);
      cursor  = metadata.lengths.m;
      url = CharArrayToString(array, cursor, metadata.lengths.u);
      cursor  = metadata.lengths.u;
      headers = CharArrayToString(array, cursor, metadata.lengths.h);
      cursor  = metadata.lengths.h;
      
      ArrayCopy(int2chars.b, array, 0, cursor, metadata.lengths.t);
      timeout = int2chars.x;
      cursor  = metadata.lengths.t;
      
      if(metadata.lengths.b > 0)
      {
        ArrayCopy(body, array, 0, cursor, metadata.lengths.b);
      }
    }
    
    ...

這里的主要工作是通過(guò)連續(xù)調(diào)用 resource.Get(array) 來(lái)進(jìn)行的,然后,會(huì)按照步驟從‘a(chǎn)rray’ 中讀取元數(shù)據(jù)字節(jié),以及隨后的欄位,

請(qǐng)求的執(zhí)行結(jié)果是使用相同方法封裝和解包的,分別使用的是 packResponse 和 unpackResponse 方法 (下面的附件中有完整代碼).

    void packResponse(const string source, const uchar &result[], const string &result_headers);     void unpackResponse(uchar &initiator[], uchar &headers[], uchar &text[]);

現(xiàn)在我們可以回到 ClientWebWorker 源代碼中并完成 'request' 和 'receiveResult' 方法了。

class ClientWebWorker : public WebWorker
{
    ...

    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      ResourceMediator mediator(allocate());
      mediator.packRequest(method, url, headers, timeout, body);
    
      busy = EventChartCustom(managerChartID, 0 /* TODO: 特定的信息 */, chartID, 0.0, getFullName());
      return busy;
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ': Reading result ', resname);
      const RESOURCEDATA<uchar> resource(resname);
      ResourceMediator mediator(&resource);
      mediator.unpackResponse(initiator, headers, text);
    }
};

它們非常簡(jiǎn)單,因?yàn)槌R?guī)工作都是由 ResourceMediator 類(lèi)來(lái)完成的。

剩下的問(wèn)題就是由誰(shuí)以及什么時(shí)候調(diào)用 WebWorker 方法, 以及我們?cè)鯓拥玫揭恍┕ぞ邊?shù)的值,例如在 ‘request’ 方法中的 managerChartID。盡管我稍微超前了一點(diǎn),我推薦把所有 WebWorker 類(lèi)對(duì)象的管理分配到更高一層的類(lèi)中,它可以支持實(shí)際對(duì)象的列表,并且在程序之間“代表”對(duì)象交換信息,包括管理器搜索信息。但是在我們轉(zhuǎn)到這一新水平之前,需要為“服務(wù)器”部分完成類(lèi)似的準(zhǔn)備工作。


2.3. 基類(lèi) (繼續(xù))

讓我們聲明從 WebWorker 派生的自定義類(lèi)來(lái)在“服務(wù)器”(管理器)端處理異步請(qǐng)求,就和客戶(hù)端的 ClientWebWorker 類(lèi)似。

class ServerWebWorker : public WebWorker {   public:     ServerWebWorker(const long id, const string p = 'WRP_'): WebWorker(id, p)     {     }          bool transfer(const string resname, const long clientChartID)     {       // 回應(yīng) `clientChartID` 的客戶(hù)端,任務(wù)名為 `resname`       // 并且把任務(wù)分配給 ID 為 `chartID` 的工作單元       busy = EventChartCustom(clientChartID, TO_MSG(MSG_ACCEPTED), chartID, 0.0, resname)           && EventChartCustom(chartID, TO_MSG(MSG_WEB), clientChartID, 0.0, resname);       return busy;     }          void receive(const string source, const uchar &result[], const string &result_headers)     {       ResourceMediator mediator(allocate());       mediator.packResponse(source, result, result_headers);     } };

'transfer' 方法根據(jù)整個(gè)交互過(guò)程中的第二步,把對(duì)請(qǐng)求的處理分發(fā)到某個(gè)輔助EA的實(shí)例中。resname 參數(shù)是從客戶(hù)端取得的資源名稱(chēng),而 clientChartID 是客戶(hù)窗口的 ID。我們從自定義事件中取得所有這些參數(shù),自定義事件本身,包括 MSG_WEB, 在下面描述。

'receive' 方法在 WebWorker 當(dāng)前對(duì)象中創(chuàng)建一個(gè)本地資源 ('allocate' 調(diào)用) 并且把名稱(chēng)寫(xiě)到原來(lái)請(qǐng)求初始的資源,另外還使用 ResourceMediator 類(lèi)的 ‘mediator' 對(duì)象把從互聯(lián)網(wǎng)上取得的數(shù)據(jù) (result) 和 HTTP 頭 (result_headers) 也寫(xiě)到資源中。這就是整體步驟中的第五步部分。

這樣,我們就為客戶(hù)端和“服務(wù)器”端都定義了 WebWorker 類(lèi),在這兩種情況中,這些對(duì)象很大程度上都很近似。例如,一個(gè)客戶(hù)可以一次下載幾個(gè)文檔,而在管理器端,一開(kāi)始最好分發(fā)足夠數(shù)量的助手,因?yàn)榭赡芡瑫r(shí)會(huì)有多個(gè)客戶(hù)發(fā)來(lái)請(qǐng)求。讓我們定義 WebWorkersPool 基類(lèi)來(lái)用于處理對(duì)象數(shù)組。讓我們把它作為模板,因?yàn)樵诳蛻?hù)端和“服務(wù)器”端保存對(duì)象的類(lèi)型是不同的 (分別對(duì)應(yīng) ClientWebWorker 和 ServerWebWorker)。

template<typename T>
class WebWorkersPool
{
  protected:
    T *workers[];
    
  public:
    WebWorkersPool() {}
    
    WebWorkersPool(const uint size)
    {
      // 分配工作單元;在客戶(hù)端它們用于在資源中保存請(qǐng)求參數(shù)
      ArrayResize(workers, size);
      for(int i = 0; i < ArraySize(workers); i  )
      {
        workers[i] = NULL;
      }
    }
    
    ~WebWorkersPool()
    {
      for(int i = 0; i < ArraySize(workers); i  )
      {
        if(CheckPointer(workers[i]) == POINTER_DYNAMIC) delete workers[i];
      }
    }
    
    int size() const
    {
      return ArraySize(workers);
    }
    
    void operator<<(T *worker)
    {
      const int n = ArraySize(workers);
      ArrayResize(workers, n   1);
      workers[n] = worker;
    }
    
    T *findWorker(const string resname) const
    {
      for(int i = 0; i < ArraySize(workers); i  )
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getFullName() == resname)
          {
            return workers[i];
          }
        }
      }
      return NULL;
    }
    
    T *getIdleWorker() const
    {
      for(int i = 0; i < ArraySize(workers); i  )
      {
        if(workers[i] != NULL)
        {
          if(ChartPeriod(workers[i].getChartID()) > 0) // check if exist
          {
            if(!workers[i].isBusy())
            {
              return workers[i];
            }
          }
        }
      }
      return NULL;
    }
    
    T *findWorker(const long id) const
    {
      for(int i = 0; i < ArraySize(workers); i  )
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getChartID() == id)
          {
            return workers[i];
          }
        }
      }
      return NULL;
    }
    
    bool revoke(const long id)
    {
      for(int i = 0; i < ArraySize(workers); i  )
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getChartID() == id)
          {
            if(CheckPointer(workers[i]) == POINTER_DYNAMIC) delete workers[i];
            workers[i] = NULL;
            return true;
          }
        }
      }
      return false;
    }
    
    int available() const
    {
      int count = 0;
      for(int i = 0; i < ArraySize(workers); i  )
      {
        if(workers[i] != NULL)
        {
          count  ;
        }
      }
      return count;
    }
    
    T *operator[](int i) const
    {
      return workers[i];
    }
    
};

方法背后的思路很簡(jiǎn)單,構(gòu)造函數(shù)和析構(gòu)函數(shù)分配和釋放指定大小處理器的數(shù)組,findWorker 和 getIdleWorker 方法組用于在數(shù)組中根據(jù)各種標(biāo)準(zhǔn)進(jìn)行搜索, 'operator<<' 操作符可以動(dòng)態(tài)增加對(duì)象,而 'revoke' 方法可以動(dòng)態(tài)刪除它們。

客戶(hù)端處理器的池有一些特殊 (特別是關(guān)于事件處理的部分). 所以,我們擴(kuò)展了基類(lèi),使用了派生的 ClientWebWorkersPool 類(lèi)。

template<typename T> class ClientWebWorkersPool: public WebWorkersPool<T> {   protected:     long   managerChartID;     short  managerPoolSize;     string name;        public:     ClientWebWorkersPool(const uint size, const string prefix): WebWorkersPool(size)     {       name = prefix;       // 嘗試尋找 WebRequest 管理器圖表       WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID());     }          bool WebRequestAsync(const string method, const string url, const string headers, int timeout, const char &data[])     {       T *worker = getIdleWorker();       if(worker != NULL)       {         return worker.request(method, url, headers, timeout, data, managerChartID);       }       return false;     }          void onChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)     {       if(MSG(id) == MSG_DONE) // 異步請(qǐng)求完成,有結(jié)果或者出錯(cuò)       {         Print(ChartID(), ': Result code ', (long)dparam);              if(sparam != NULL)         {           // 從資源中根據(jù) sparam 中的名稱(chēng)讀取數(shù)據(jù)           uchar initiator[], headers[], text[];           ClientWebWorker::receiveResult(sparam, initiator, headers, text);           string resname = CharArrayToString(initiator);                      T *worker = findWorker(resname);           if(worker != NULL)           {             worker.onResult((long)dparam, headers, text);             worker.release();           }         }       }              ...              else       if(MSG(id) == MSG_HELLO) // MSG_DISCOVER 廣播結(jié)果找到了管理器       {         if(managerChartID == 0 && lparam != 0)         {           if(ChartPeriod(lparam) > 0)           {             managerChartID = lparam;             managerPoolSize = (short)dparam;             for(int i = 0; i < ArraySize(workers); i )             {               workers[i] = new T(ChartID(), name (string)(i 1) '_');             }           }         }       }     }          bool isManagerBound() const     {       return managerChartID != 0;     } };

變量描述:

  • managerChartID — 找到的工作管理器窗口的 ID ;
  • managerPoolSize — 處理對(duì)象數(shù)組的初始大小;
  • name — 所有池中對(duì)象資源的通用前綴。


2.4. 交換消息

在 ClientWebWorkersPool 構(gòu)造函數(shù)中, 我們看到了對(duì) WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()) 的調(diào)用,它發(fā)送 MSG_DISCOVER 事件到所有的窗口,在事件參數(shù)中傳入當(dāng)前窗口的 ID。MSG_DISCOVER 是一個(gè)保留值: 它應(yīng)當(dāng)定義在相同頭文件的開(kāi)始,同時(shí)還有其它程序要交換的消息類(lèi)型。

#define MSG_DEINIT   1 // 銷(xiāo)毀 (管理器 <-> 工作單元)
#define MSG_WEB      2 // 開(kāi)始請(qǐng)求 (客戶(hù) -> 管理器 -> 工作單元)
#define MSG_DONE     3 // 請(qǐng)求結(jié)束 (工作單元 -> 客戶(hù), 工作單元 -> 管理器)
#define MSG_ERROR    4 // 請(qǐng)求失敗 (管理器 -> 客戶(hù), 工作單元 -> 客戶(hù))
#define MSG_DISCOVER 5 // 尋找管理器 (客戶(hù) -> 管理器)
#define MSG_ACCEPTED 6 // 請(qǐng)求正在進(jìn)行 (管理器 -> 客戶(hù))
#define MSG_HELLO    7 // 找到了管理器 (管理器 -> 客戶(hù))

注釋中標(biāo)記了消息發(fā)送的方向。

TO_MSG 宏定義是設(shè)計(jì)用于把列出的ID轉(zhuǎn)換為用戶(hù)隨機(jī)選擇基礎(chǔ)值的實(shí)際事件代碼,我們將通過(guò) MessageBroadcast 輸入?yún)?shù)得到它。

sinput uint MessageBroadcast = 1; #define TO_MSG(X) ((ushort)(MessageBroadcast X))

這種方法可以把所有事件都通過(guò)修改基礎(chǔ)值而轉(zhuǎn)換到任何空閑范圍。請(qǐng)注意,自定義事件可以在終端中由其它程序使用,所以,避免沖突是很重要的。

MessageBroadcast 輸入?yún)?shù)將出現(xiàn)在我們所有使用 multiweb.mqh 文件的 MQL 程序中,也就是客戶(hù)和管理器中。在管理器和客戶(hù)中應(yīng)當(dāng)指定相同的 MessageBroadcast 值。

讓我們回到 ClientWebWorkersPool 類(lèi),onChartEvent 方法占有特殊的位置,它將從標(biāo)準(zhǔn)的 OnChartEvent 事件處理函數(shù)中調(diào)用。事件的類(lèi)型通過(guò)‘id'參數(shù)來(lái)傳遞。因?yàn)槲覀儚南到y(tǒng)中根據(jù)選定的基礎(chǔ)數(shù)值接收代碼,我們應(yīng)當(dāng)使用“鏡像”的 MSG 宏來(lái)把它轉(zhuǎn)回到 MSG_*** 范圍:

#define MSG(x) (x - MessageBroadcast - CHARTEVENT_CUSTOM)

這里 CHARTEVENT_CUSTOM 就是終端中所有自定義事件的開(kāi)始。

我們可以看到,在 ClientWebWorkersPool 中的 onChartEvent 方法處理了以上描述的一些消息,例如,管理器應(yīng)當(dāng)對(duì) MSG_HELLO 做出回應(yīng)而返回 MSG_DISCOVER 消息。在這種情況下,管理器窗口ID是在 lparam 參數(shù)中傳遞的,而可用的助手?jǐn)?shù)量是在 dparam 參數(shù)中傳遞的。當(dāng)管理器被偵測(cè)到,池使用所需類(lèi)型的真實(shí)對(duì)象來(lái)填充空白的’workers'數(shù)組。當(dāng)前窗口的ID,以及每個(gè)對(duì)象中的獨(dú)有資源名稱(chēng)就傳遞給對(duì)象的構(gòu)造函數(shù),后者包含了通用的‘name'前綴和在數(shù)組中的序列號(hào)。

在 managerChartID 欄位收到了有意義的數(shù)值后,就可以發(fā)送請(qǐng)求到管理器了。'request' 方法就是在 ClientWebWorker 類(lèi)中為此保留的,而它的用法展示在 WebRequestAsync 方法中。首先,WebRequestAsync 使用 getIdleWorkder 來(lái)找到一個(gè)空閑的處理器對(duì)象,然后再調(diào)用 worker.request(method, url, headers, timeout, data, managerChartID)。在 'request' 方法內(nèi)部,我們有一個(gè)關(guān)于選擇特定消息代碼來(lái)發(fā)送事件的注釋?,F(xiàn)在,在探討了事件子系統(tǒng)之后,我們可以構(gòu)建最終版本的 ClientWebWorker::request 方法了:

class ClientWebWorker : public WebWorker {     ...     bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)     {       _method = method;       _url = url;       ResourceMediator mediator(allocate());       mediator.packRequest(method, url, headers, timeout, body);            busy = EventChartCustom(managerChartID, TO_MSG(MSG_WEB), chartID, 0.0, getFullName());       return busy;     }          ... };

MSG_WEB 是關(guān)于執(zhí)行web請(qǐng)求的消息,在接收到它之后,管理器應(yīng)當(dāng)找到一個(gè)空閑的助手EA,并向它傳遞客戶(hù)資源名稱(chēng) (sparam) 以及請(qǐng)求的參數(shù),還有 chartID (lparam),即客戶(hù)窗口 ID。

助手執(zhí)行請(qǐng)求,并使用 MSG_DONE 事件把結(jié)果返回給客戶(hù) (如果成功) 或者使用 MSG_ERROR 返回錯(cuò)誤代碼(如果出了問(wèn)題)。結(jié)果 (或者錯(cuò)誤) 代碼是傳到 dparam 的, 而結(jié)果本身是打包到位于助手EA的資源中,而名稱(chēng)會(huì)傳遞給 sparam。在 MSG_DONE 分支,我們看到數(shù)據(jù)是如何從資源中讀取的,調(diào)用的是之前探討過(guò)的 ClientWebWorker::receiveResult(sparam, initiator, headers, text) 函數(shù)。然后,執(zhí)行搜索客戶(hù)處理器對(duì)象 (findWorker) ,根據(jù)的是請(qǐng)求的資源名稱(chēng),再在偵測(cè)到的對(duì)象中執(zhí)行一系列方法:

    T *worker = findWorker(resname);
    if(worker != NULL)
    {
      worker.onResult((long)dparam, headers, text);
      worker.release();
    }

我們已經(jīng)知道了 'release' 方法 — 它可以釋放已經(jīng)不再需要的資源。什么是 onResult?如果我們看完整的源代碼, 我們將看到 ClientWebWorker 類(lèi)中含有兩個(gè)沒(méi)有實(shí)現(xiàn)的虛擬函數(shù): onResult and onError. 這使得類(lèi)稱(chēng)為抽象類(lèi)??蛻?hù)代碼應(yīng)當(dāng)在從 ClientWebWorker 類(lèi)派生時(shí)提供實(shí)現(xiàn)。方法的名稱(chēng)提示了,如果成功接收到結(jié)果,就調(diào)用 onResult,而如果出錯(cuò)就調(diào)用 onError。這可以使異步請(qǐng)求的工作類(lèi)和使用它們的客戶(hù)程序代碼之間提供反饋,換句話說(shuō),客戶(hù)程序不需要知道關(guān)于消息在核心內(nèi)部使用的任何事情:所有客戶(hù)代碼的交互都已經(jīng)在 MQL5 OOP 內(nèi)建工具中提供了。

讓我們看一下客戶(hù)端的源代碼 (multiwebclient.mq5)。


2.5. 客戶(hù) EA

測(cè)試的EA會(huì)通過(guò) multiweb API 根據(jù)用戶(hù)輸入的數(shù)據(jù)發(fā)送幾個(gè)請(qǐng)求,為此,我們需要包含頭文件并增加輸入?yún)?shù):

sinput string Method = 'GET'; sinput string URL = 'https://google.com/,https://,https://www./'; sinput string Headers = 'User-Agent: n/a'; sinput int Timeout = 5000; #include <multiweb.mqh>

最終,所有參數(shù)都是用于配置進(jìn)行 HTTP 請(qǐng)求的,在 URL 列表中,我們可以列出幾個(gè)逗號(hào)分隔的地址,以評(píng)估并行執(zhí)行請(qǐng)求的速度。URL 參數(shù)在 OnInit 中使用 StringSplit 函數(shù)分成幾段,就像這樣:

int urlsnum;
string urls[];
  
void OnInit()
{
  // 取得用于測(cè)試請(qǐng)求的URL
  urlsnum = StringSplit(URL, ',', urls);
  ...
}

另外,我們需要在OnInit 中創(chuàng)建一個(gè)請(qǐng)求處理對(duì)象的池 (ClientWebWorkersPool) ,但是為了做到這一點(diǎn),我們需要從 ClientWebWorker 類(lèi)中派生我們的類(lèi)。

class MyClientWebWorker : public ClientWebWorker {   public:     MyClientWebWorker(const long id, const string p = 'WRP_'): ClientWebWorker(id, p)     {     }          virtual void onResult(const long code, const uchar &headers[], const uchar &text[]) override     {       Print(getMethod(), ' ', getURL(), '\nReceived ', ArraySize(headers), ' bytes in header, ', ArraySize(text), ' bytes in document');       // 不注釋掉這個(gè)會(huì)導(dǎo)致可能有過(guò)多記錄       // Print(CharArrayToString(headers));       // Print(CharArrayToString(text));     }     virtual void onError(const long code) override     {       Print('WebRequest error code ', code);     } };

它的唯一目標(biāo)是記錄狀態(tài)和取得的數(shù)據(jù),現(xiàn)在我們可以在OnInit 中為這樣的對(duì)象創(chuàng)建一個(gè)池。

ClientWebWorkersPool<MyClientWebWorker> *pool = NULL;

void OnInit()
{
  ...
  pool = new ClientWebWorkersPool<MyClientWebWorker>(urlsnum, _Symbol   '_'   EnumToString(_Period)   '_');
  Comment('Click the chart to start downloads');
}

您可以看到,池是使用了 MyClientWebWorker 類(lèi)參數(shù)化的,這就可以從開(kāi)發(fā)庫(kù)代碼中創(chuàng)建我們的對(duì)象了。數(shù)組的大小選擇等于輸入地址的數(shù)量。這對(duì)演示目的是合理的:更小的數(shù)量表示處理隊(duì)列正在處理而會(huì)破壞并行執(zhí)行的思路,而更大的數(shù)量則會(huì)浪費(fèi)資源。在真實(shí)項(xiàng)目中,池的大小不一定要等于任務(wù)的數(shù)量,但是這需要另外的數(shù)學(xué)驗(yàn)證。

資源的前綴設(shè)為所操作交易品種和圖表時(shí)段的組合。

初始化的最后部分是搜索管理器窗口,您也許記得,搜索是在池自身中進(jìn)行的 (ClientWebWorkersPool 類(lèi)),客戶(hù)代碼只要確保能找到管理器。為此,讓我們?cè)O(shè)置合理的時(shí)間,等待關(guān)于搜索管理器的消息,就應(yīng)該能夠保證得到回應(yīng)。讓我們把它設(shè)為5秒,為這個(gè)時(shí)間創(chuàng)建一個(gè)計(jì)時(shí)器:

void OnInit() {   ...   // 等待管理器有5秒最大的討論時(shí)間   EventSetTimer(5); }

檢查在計(jì)時(shí)器處理函數(shù)中是否有管理器出現(xiàn)。如果連接沒(méi)有建立好,就顯示一個(gè)提醒。

void OnTimer()
{
  // 如果管理器在5秒中內(nèi)都沒(méi)有回應(yīng),看起來(lái)就丟失了。
  EventKillTimer();
  if(!pool.isManagerBound())
  {
    Alert('WebRequest Pool Manager (multiweb) is not running');
  }
}

不要忘記在 OnDeinit 處理函數(shù)中刪除池對(duì)象。

void OnDeinit(const int reason) {   delete pool;   Comment(''); }

為了讓池來(lái)處理所有的服務(wù)消息而不需要我們的介入,首先,搜索管理器,使用的是標(biāo)準(zhǔn)的 OnChartEvent 圖表事件處理函數(shù):

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) // 通過(guò)簡(jiǎn)單的用戶(hù)操作初始化測(cè)試請(qǐng)求
  {
    ...
  }
  else
  {
    // 這個(gè)處理函數(shù)管理幕后所有重要的消息
    pool.onChartEvent(id, lparam, dparam, sparam);
  }
}

所有的事件,除了 CHARTEVENT_CLICK 之外, 都發(fā)送到池中,根據(jù)分析所使用事件的代碼再執(zhí)行相應(yīng)的操作 ( onChartEvent 代碼片段上面已經(jīng)提供了).

CHARTEVENT_CLICK 事件是交互的,直接用于運(yùn)行下載,最簡(jiǎn)單的情況下,它可能看起來(lái)如下:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {   if(id == CHARTEVENT_CLICK) // 通過(guò)簡(jiǎn)單的用戶(hù)操作初始化測(cè)試請(qǐng)求   {     if(pool.isManagerBound())     {       uchar Body[];       for(int i = 0; i < urlsnum; i )       {         pool.WebRequestAsync(Method, urls[i], Headers, Timeout, Body);       }     }     ...

實(shí)例的完整代碼有些長(zhǎng),因?yàn)樗€包含著計(jì)算執(zhí)行時(shí)間并把它與對(duì)相同地址集合串行調(diào)用標(biāo)準(zhǔn) WebRequest 來(lái)作比較。


2.6. 管理器 EA 和助手 EA

我們終于到了”服務(wù)器“部分,因?yàn)榛緳C(jī)制已經(jīng)在頭文件中實(shí)現(xiàn),管理器和助手的代碼就沒(méi)有想象得那樣復(fù)雜了,

您也許記得,我們只有一個(gè)EA,它既以管理器工作,也可以作為助手 (multiweb.mq5 文件)。作為客戶(hù),我們包含頭文件并聲明輸入?yún)?shù):

sinput uint WebRequestPoolSize = 3;
sinput ulong ManagerChartID = 0;

#include <multiweb.mqh>

WebRequestPoolSize 是管理器應(yīng)當(dāng)創(chuàng)建輔助窗口的數(shù)量,再在其中運(yùn)行助手,

ManagerChartID 是管理器窗口 ID,這個(gè)參數(shù)只有在助手中可以使用,是在助手從代碼中自動(dòng)運(yùn)行的時(shí)候要填充到管理器中的。當(dāng)運(yùn)行管理器時(shí)人工填寫(xiě) ManagerChartID 會(huì)被認(rèn)為出錯(cuò)。

算法是基于兩個(gè)全局變量構(gòu)建的:

bool manager; WebWorkersPool<ServerWebWorker> pool;

'manager' 本地標(biāo)志指示了當(dāng)前EA實(shí)例的角色,'pool' 變量是用于到來(lái)任務(wù)的處理器對(duì)象的數(shù)組。WebWorkersPool 是通過(guò)上面所描述的 ServerWebWorker 類(lèi)來(lái)分類(lèi)的。數(shù)組沒(méi)有進(jìn)一步初始化,因?yàn)樗歉鶕?jù)角色填充的。

第一個(gè)運(yùn)行的實(shí)例 (在 OnInit 中定義) 得到的是管理器角色。

const string GVTEMP = 'WRP_GV_TEMP';

int OnInit()
{
  manager = false;
  
  if(!GlobalVariableCheck(GVTEMP))
  {
    // 當(dāng)開(kāi)始啟動(dòng) multiweb 的第一個(gè)實(shí)例是,它被當(dāng)成管理器
    // 全局變量是管理器存在的標(biāo)志
    if(!GlobalVariableTemp(GVTEMP))
    {
      FAILED(GlobalVariableTemp);
      return INIT_FAILED;
    }
    
    manager = true;
    GlobalVariableSet(GVTEMP, 1);
    Print('WebRequest Pool Manager started in ', ChartID());
  }
  else
  {
    // 所有隨后的 multiweb 實(shí)例都是工作單元/助手
    Print('WebRequest Worker started in ', ChartID(), '; manager in ', ManagerChartID);
  }
  
  // 使用計(jì)時(shí)器來(lái)延遲工作單元的初始化
  EventSetTimer(1);
  return INIT_SUCCEEDED;
}

EA 檢查終端中是否有特定的全局變量,如果沒(méi)有,EA 會(huì)把自身賦為管理器,并創(chuàng)建這樣的一個(gè)全局變量。如果變量已經(jīng)存在,那么這就是有管理器,所以這個(gè)實(shí)例就變成一個(gè)助手。請(qǐng)注意,全局變量是臨時(shí)的,也就是說(shuō)在終端重新啟動(dòng)的時(shí)候它不會(huì)被保存,但是如果管理器保留在任何圖表上,它會(huì)再次創(chuàng)建這個(gè)變量。

計(jì)時(shí)器設(shè)為1秒,因?yàn)檩o助圖表的初始化會(huì)花好幾秒,在 OnInit 中做這些不是最好的方案。在計(jì)時(shí)器事件處理函數(shù)中填充池:

void OnTimer() {   EventKillTimer();   if(manager)   {     if(!instantiateWorkers())     {       Alert('Workers not initialized');     }     else     {       Comment('WebRequest Pool Manager ', ChartID(), '\nWorkers available: ', pool.available());     }   }   else // 工作單元   {     // 這是用于資源的宿主,保存回應(yīng)的頭部和數(shù)據(jù)     pool << new ServerWebWorker(ChartID(), 'WRR_');   } }

如果是助手角色,就簡(jiǎn)單把另一個(gè) ServerWebWorker 處理器對(duì)象加到數(shù)組中管理器的情況更加復(fù)雜,要在獨(dú)立的 instantiateWorkers 函數(shù)中處理,讓我們看看它。

bool instantiateWorkers()
{
  MqlParam Params[4];
  
  const string path = MQLInfoString(MQL_PROGRAM_PATH);
  const string experts = '\\MQL5\\';
  const int pos = StringFind(path, experts);
  
  // 再次啟動(dòng)自身 (以助手EA的角色)
  Params[0].string_value = StringSubstr(path, pos   StringLen(experts));
  
  Params[1].type = TYPE_UINT;
  Params[1].integer_value = 1; //  新的助手EA實(shí)例中有一個(gè)工作單元,用于返回結(jié)果到管理器或客戶(hù)

  Params[2].type = TYPE_LONG;
  Params[2].integer_value = ChartID(); // 這個(gè)圖表是管理器

  Params[3].type = TYPE_UINT;
  Params[3].integer_value = MessageBroadcast; // 使用相同的自定義事件基礎(chǔ)編號(hào)
  
  for(uint i = 0; i < WebRequestPoolSize;   i)
  {
    long chart = ChartOpen(_Symbol, _Period);
    if(chart == 0)
    {
      FAILED(ChartOpen);
      return false;
    }
    if(!EXPERT::Run(chart, Params))
    {
      FAILED(EXPERT::Run);
      return false;
    }
    pool << new ServerWebWorker(chart);
  }
  return true;
}

這個(gè)函數(shù)使用了第三方的 Expert 開(kāi)發(fā)庫(kù),是由我們的老朋友 - MQL5 社區(qū)成員 fxsaber 開(kāi)發(fā)的, 所以在源代碼的開(kāi)頭加入了對(duì)應(yīng)的頭文件。

#include <fxsaber\Expert.mqh>

Expert 開(kāi)發(fā)庫(kù)允許您動(dòng)態(tài)生成 tpl 模板和指定的 EA 參數(shù),并且把它們應(yīng)用到圖表上,就能夠載入 EA,在我們的實(shí)例中,所有助手EA的參數(shù)都是相同的,所以它們的列表在創(chuàng)建指定數(shù)量的窗口之前就生成了,

參數(shù) 0 指定了執(zhí)行EA文件的路徑,也就是它自己。參數(shù) 1 是 WebRequestPoolSize,它在每個(gè)助手中都等于1. 我已經(jīng)提過(guò),處理器對(duì)象在助手中只是用于保存 HTTP 請(qǐng)求結(jié)果的資源的,每個(gè)助手都通過(guò)阻塞式的 WebRequest 處理請(qǐng)求,也就是最多使用一個(gè)處理器對(duì)象。參數(shù) 2 — ManagerChartID 管理器窗口 ID. 參數(shù) 3 — 消息代碼的基礎(chǔ)數(shù)值 (MessageBroadcast 參數(shù)是來(lái)自 multiweb.mqh).

另外,在循環(huán)中使用了 ChartOpen 來(lái)創(chuàng)建了空白的圖表,并且使用 EXPERT::Run (chart, Params) 來(lái)在其中運(yùn)行 EA。ServerWebWorker(chart) 處理器對(duì)象在每個(gè)新窗口中創(chuàng)建,并加入池中。在管理器中,處理器對(duì)象就像助手窗口 ID 的連接和它們的狀態(tài),因?yàn)?HTTP 請(qǐng)求不是在管理器自身中進(jìn)行的,而不會(huì)為它們創(chuàng)建資源。

來(lái)臨的任務(wù)是根據(jù)用于在 OnChartEvent 中的事件來(lái)處理的。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(MSG(id) == MSG_DISCOVER) // 在新的客戶(hù)圖表中初始化一個(gè)工作單元EA并綁定到管理器
  {
    if(manager && (lparam != 0))
    {
      // 只有管理器使用它的圖表ID回應(yīng),lparam 是客戶(hù)圖表 ID
      EventChartCustom(lparam, TO_MSG(MSG_HELLO), ChartID(), pool.available(), NULL);
    }
  }
  else
  if(MSG(id) == MSG_WEB) // 已經(jīng)有了請(qǐng)求 web 下載的客戶(hù)端
  {
    if(lparam != 0)
    {
      if(manager)
      {
        // 管理器把工作分發(fā)到空閑工作單元
        // lparam 是客戶(hù)圖表ID,而 sparam 是客戶(hù)資源
        if(!transfer(lparam, sparam))
        {
          EventChartCustom(lparam, TO_MSG(MSG_ERROR), ERROR_NO_IDLE_WORKER, 0.0, sparam);
        }
      }
      else
      {
        // 工作單元實(shí)際處理 web 請(qǐng)求
        startWebRequest(lparam, sparam);
      }
    }
  }
  else
  if(MSG(id) == MSG_DONE) // 一個(gè)根據(jù)在 lparam 中的圖表 ID 識(shí)別到的工作單元完成了工作
  {
    WebWorker *worker = pool.findWorker(lparam);
    if(worker != NULL)
    {
      // 我們這里是在管理器中,并且池中只保存工作單元而沒(méi)有資源,
      // 所以這個(gè) release 只是用于清除繁忙狀態(tài)
      worker.release();
    }
  }
}

首先,為了回應(yīng)來(lái)自客戶(hù)端帶有 lparam ID 的 MSG_DISCOVER 消息,管理器要返回含有它窗口 ID 的 MSG_HELLO 消息,

根據(jù)接收到的 MSG_WEB, lparam 應(yīng)當(dāng)包含發(fā)送請(qǐng)求的客戶(hù)的窗口 ID,而 sparam 應(yīng)當(dāng)包含打包了請(qǐng)求參數(shù)的資源的名稱(chēng)。作為管理器工作,代碼會(huì)把包含這些參數(shù)的任務(wù)傳遞給一個(gè)空閑助手,調(diào)用的是 'transfer' 函數(shù) (下面會(huì)描述) 并把選中對(duì)象的狀態(tài)設(shè)為 'busy(繁忙)'。如果沒(méi)有空閑的助手,就向客戶(hù)發(fā)送 MSG_ERROR 事件,代碼為 ERROR_NO_IDLE_WORKER。助手在 startWebRequest 函數(shù)中執(zhí)行 HTTP 請(qǐng)求。

當(dāng)上傳了所需的文檔時(shí),會(huì)從助手到管理器發(fā)送 MSG_DONE 消息,管理器從中根據(jù) lparam 中的助手ID來(lái)尋找對(duì)應(yīng)的對(duì)象,并通過(guò)調(diào)用 ‘release' 方法修改它的“busy'狀態(tài)。已經(jīng)說(shuō)過(guò),助手會(huì)把運(yùn)行的結(jié)果直接發(fā)送給客戶(hù)。

完整的代碼中也包含了 MSG_DEINIT 事件,它與 OnDeinit 的處理相關(guān)。思路是,助手在管理器刪除時(shí)被通知,然后自己退出并關(guān)閉它們的窗口,而再通知管理器助手已經(jīng)被刪除,并從管理器池中刪除它。我相信,您可以自己了解其中的機(jī)制。

'transfer' 函數(shù)搜索空閑對(duì)象,并調(diào)用它的 'transfer' 方法 (上面討論過(guò))。

bool transfer(const long returnChartID, const string resname) {   ServerWebWorker *worker = pool.getIdleWorker();   if(worker == NULL)   {     return false;   }   return worker.transfer(resname, returnChartID); }

startWebRequest 函數(shù)描述如下:

void startWebRequest(const long returnChartID, const string resname)
{
  const RESOURCEDATA<uchar> resource(resname);
  ResourceMediator mediator(&resource);

  string method, url, headers;
  int timeout;
  uchar body[];

  mediator.unpackRequest(method, url, headers, timeout, body);

  char result[];
  string result_headers;
  
  int code = WebRequest(method, url, headers, timeout, body, result, result_headers);
  if(code != -1)
  {
    // 使用結(jié)果創(chuàng)建資源,通過(guò)自定義事件傳回客戶(hù)端
    ((ServerWebWorker *)pool[0]).receive(resname, result, result_headers);
    // 首先,向客戶(hù)發(fā)送 MSG_DONE,包括結(jié)果資源。
    EventChartCustom(returnChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, pool[0].getFullName());
    // 第二, 發(fā)送 MSG_DONE 到管理器,把對(duì)應(yīng)工作單元設(shè)為空閑狀態(tài)
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, NULL);
  }
  else
  {
    // 錯(cuò)誤代碼在 dparam 中
    EventChartCustom(returnChartID, TO_MSG(MSG_ERROR), ERROR_MQL_WEB_REQUEST, (double)GetLastError(), resname);
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)GetLastError(), NULL);
  }
}

通過(guò)使用 ResourceMediator, 這個(gè)函數(shù)解包取得請(qǐng)求的參數(shù)并調(diào)用標(biāo)準(zhǔn)的 MQL WebRequest 函數(shù),如果函數(shù)執(zhí)行沒(méi)有出現(xiàn) MQL 錯(cuò)誤,就把結(jié)果發(fā)送給客戶(hù)。為此,使用 ’receive' 方法把它們打包到本地資源中,而它的名稱(chēng)與 MSG_DONE 消息在 EventChartCustom 函數(shù)的 sparam 參數(shù)中傳遞。請(qǐng)注意,HTTP 錯(cuò)誤 (例如,無(wú)效頁(yè)面 404 或者 web 服務(wù)器錯(cuò)誤 501) 也會(huì)在這里出現(xiàn) — 客戶(hù)可以在 dparam 參數(shù)中收到 HTTP 代碼,在資源中收到回應(yīng)的 HTTP 頭部,這可以讓我們進(jìn)一步進(jìn)行分析。

如果 WebRequest 調(diào)用以 MQL 錯(cuò)誤結(jié)束, 客戶(hù)會(huì)收到 MSG_ERROR 消息和 ERROR_MQL_WEB_REQUEST 代碼, 而 GetLastError 的結(jié)果放在 dparam 中。因?yàn)檫@種情況下本地資源沒(méi)有填充,來(lái)源資源的名稱(chēng)會(huì)直接放在 sparam 參數(shù)中,這樣處理對(duì)象的某個(gè)實(shí)例還是能在客戶(hù)端識(shí)別。

用于異步和并行調(diào)用 WebRequest 的 multiweb 開(kāi)發(fā)庫(kù)類(lèi)圖

圖 4. 用于異步和并行調(diào)用 WebRequest 的 multiweb 開(kāi)發(fā)庫(kù)類(lèi)圖


3. 測(cè)試

可以按照如下方式測(cè)試所實(shí)現(xiàn)的軟件。

首先,打開(kāi)終端設(shè)置,并在專(zhuān)家頁(yè)面中允許訪問(wèn)的URL列表中指定所有可以訪問(wèn)的服務(wù)器。

然后,運(yùn)行 multiweb EA 并且在輸入?yún)?shù)中設(shè)置三個(gè)助理。這樣,可以打開(kāi)三個(gè)新的窗口,含有角色不同的相同的 multiweb EA。EA 角色顯示在窗口左上角的注釋中。

現(xiàn)在,讓我們?cè)诹硪粋€(gè)圖表中運(yùn)行 multiwebclient 客戶(hù)端 EA,并在圖表上點(diǎn)擊一次,使用默認(rèn)的設(shè)置,它會(huì)啟動(dòng)三個(gè)并行的 web request 并把診斷信息寫(xiě)到記錄中,包括取得數(shù)據(jù)的大小和運(yùn)行時(shí)間。如果 TestSyncRequests 特定參數(shù)保持為 'true', 就會(huì)通過(guò)管理器使用標(biāo)準(zhǔn) WebRequest 在并行 web request 之外再順序進(jìn)行同樣的請(qǐng)求,這樣做是為了比較兩種選項(xiàng)的執(zhí)行速度。按照原則,并行處理會(huì)比串行處理快幾倍 - 從 sqrt(N) 到 N, 其中 N 是可用助手的數(shù)量。

示例記錄顯示如下:

01:16:50.587    multiweb (EURUSD,H1)    OnInit 129912254742671339 01:16:50.587    multiweb (EURUSD,H1)    WebRequest Pool Manager started in 129912254742671339 01:16:52.345    multiweb (EURUSD,H1)    OnInit 129912254742671345 01:16:52.345    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671345; manager in 129912254742671339 01:16:52.757    multiweb (EURUSD,H1)    OnInit 129912254742671346 01:16:52.757    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671346; manager in 129912254742671339 01:16:53.247    multiweb (EURUSD,H1)    OnInit 129912254742671347 01:16:53.247    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671347; manager in 129912254742671339 01:17:16.029    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862 01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862 01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: Got 64 bytes in request 01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: GET https://google.com/ User-Agent: n/a 5000 01:17:16.030    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862 01:17:16.030    multiweb (EURUSD,H1)    129912254742671346: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862 01:17:16.030    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862 after 0 retries 01:17:16.031    multiweb (EURUSD,H1)    129912254742671346: Got 60 bytes in request 01:17:16.031    multiweb (EURUSD,H1)    129912254742671346: GET https:// User-Agent: n/a 5000 01:17:16.031    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862 01:17:16.031    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862 after 0 retries 01:17:16.031    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862 after 0 retries 01:17:16.031    multiweb (EURUSD,H1)    129912254742671347: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862 01:17:16.032    multiweb (EURUSD,H1)    129912254742671347: Got 72 bytes in request 01:17:16.032    multiweb (EURUSD,H1)    129912254742671347: GET https://www./ User-Agent: n/a 5000 01:17:16.296    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200 01:17:16.296    multiweb (EURUSD,H1)    Result code from 129912254742671346: 200, now idle 01:17:16.297    multiweb (EURUSD,H1)    129912254742671346: Done in 265ms 01:17:16.297    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671346 01:17:16.300    multiwebclient (GBPJPY,M5)      129560567193673862: Got 16568 bytes in response 01:17:16.300    multiwebclient (GBPJPY,M5)      GET https:// 01:17:16.300    multiwebclient (GBPJPY,M5)      Received 3704 bytes in header, 12775 bytes in document 01:17:16.715    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200 01:17:16.715    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671347 01:17:16.715    multiweb (EURUSD,H1)    129912254742671347: Done in 686ms 01:17:16.715    multiweb (EURUSD,H1)    Result code from 129912254742671347: 200, now idle 01:17:16.725    multiwebclient (GBPJPY,M5)      129560567193673862: Got 45236 bytes in response 01:17:16.725    multiwebclient (GBPJPY,M5)      GET https://www./ 01:17:16.725    multiwebclient (GBPJPY,M5)      Received 822 bytes in header, 44325 bytes in document 01:17:16.900    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200 01:17:16.900    multiweb (EURUSD,H1)    Result code from 129912254742671345: 200, now idle 01:17:16.900    multiweb (EURUSD,H1)    129912254742671345: Done in 873ms 01:17:16.900    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671345 01:17:16.903    multiwebclient (GBPJPY,M5)      129560567193673862: Got 13628 bytes in response 01:17:16.903    multiwebclient (GBPJPY,M5)      GET https://google.com/ 01:17:16.903    multiwebclient (GBPJPY,M5)      Received 790 bytes in header, 12747 bytes in document 01:17:16.903    multiwebclient (GBPJPY,M5)      > > > Async WebRequest workers [3] finished 3 tasks in 873ms

請(qǐng)注意,所有請(qǐng)求的總執(zhí)行時(shí)間等于最慢的請(qǐng)求所執(zhí)行的時(shí)間,

如果我們?cè)诠芾砥髦邪阎謹(jǐn)?shù)量設(shè)為1,請(qǐng)求就被串行處理。


結(jié)論

在這篇文章中,我們探討了一些類(lèi)和完成的EA交易,它們是用于在非阻塞模式下執(zhí)行 HTTP 請(qǐng)求的。這使我們可以從互聯(lián)網(wǎng)中以幾個(gè)并行線程的方式取得數(shù)據(jù),從而提高了EA的效率,除了處理HTTP請(qǐng)求,也可以實(shí)時(shí)進(jìn)行分析計(jì)算。另外,這個(gè)開(kāi)發(fā)庫(kù)還可以用于禁止使用標(biāo)準(zhǔn) WebRequest 的指標(biāo)中。為了實(shí)現(xiàn)整個(gè)架構(gòu),我們使用了很廣范圍的 MQL 特性:傳遞用戶(hù)事件,創(chuàng)建資源,動(dòng)態(tài)開(kāi)啟窗口并在其中運(yùn)行EA等等。

在寫(xiě)這篇文章時(shí),創(chuàng)建輔助窗口用于載入助手EA交易只是實(shí)現(xiàn)并行 HTTP 請(qǐng)求的一個(gè)僅有選項(xiàng),但是 MetaQuotes 有計(jì)劃開(kāi)發(fā)特別的后臺(tái) MQL 程序。MQL5/Services 文件下已經(jīng)為這樣的服務(wù)做了保留。當(dāng)這種技術(shù)在終端中出現(xiàn)時(shí),這個(gè)開(kāi)發(fā)庫(kù)可能就可以通過(guò)使用服務(wù)來(lái)替換輔助窗口來(lái)進(jìn)行改進(jìn)了。

附件中的文件:

  • MQL5/Include/multiweb.mqh — 開(kāi)發(fā)庫(kù)
  • MQL5/Experts/multiweb.mq5 — 管理器 EA 和助手 EA 
  • MQL5/Experts/multiwebclient.mq5 — 演示的客戶(hù)端 EA
  • MQL5/Include/fxsaber/Resource.mqh — 用于操作資源的輔助類(lèi)
  • MQL5/Include/fxsaber/ResourceData.mqh — 用于操作資源的輔助類(lèi)
  • MQL5/Include/fxsaber/Expert.mqh — 用于運(yùn)行EA的輔助類(lèi)
  • MQL5/Include/TypeToBytes.mqh — 數(shù)據(jù)轉(zhuǎn)換開(kāi)發(fā)庫(kù)

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多