请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3364694 新帖:15

使用命名管道与 MetaTrader 5 进行无 DLL 通信

到此一游发表于:4 月 17 日 18:44回复(1)

简介

许多开发人员面临同样的问题 - 如何在不使用不安全 DLL 的情况下到达交易端沙箱。

一种最简单和最安全的方法是使用作为普通文件操作的标准命名管道。它们允许您组织程序之间的处理器间客户端-服务器通信。虽然《一个使用命名管道在 MetaTrader 5 客户端之间进行通信的无 DLL 解决方案》一文已经就此主题说明了对 DLL 访问的启用,我们将使用客户端的标准和安全的功能。

您可以在 MSDN 库中找到有关命名管道的更多信息,但我们将着手处理 C++ 和 MQL5 实例。我们将实施服务器、客户端、其间的数据交换以及性能基准。

服务器实施

我们以 C++ 编写一个简单的服务器。一个来自终端的脚本将连接到此服务器并与其交换数据。服务器核心具有下述 WinAPI 函数组:

  • CreateNamedPipe - 创建一个命名管道。
  • ConnectNamedPipe - 启用服务器以等待客户端连接。
  • WriteFile - 向管道写入数据。
  • ReadFile - 从管道读取数据。
  • FlushFileBuffers - 清除累积的缓冲区。
  • DisconnectNamedPipe - 断开服务器的连接。
  • CloseHandle - 关闭句柄。

打开一个命名管道后,它将返回一个文件句柄,用于常规的读/写文件操作。结果,您获得一个十分简单的机制,不需要任何网络操作的专门知识。

命名管道具有一个区别性特征 - 它们可以是本地的也可以是网络的。这就是说,实施远程服务器是很容易的,只要服务器与客户端通过网络连接。

下面是创建一个本地服务器的简单示例,该服务器作为全双工信道以字节交换模式工作:

//--- open 
CPipeManager manager;

if(!manager.Create(L"\\\\.\\pipe\\MQL5.Pipe.Server"))
   return(-1);


//+------------------------------------------------------------------+
//| Create named pipe                                                |
//+------------------------------------------------------------------+
bool CPipeManager::Create(LPCWSTR pipename)
  {
//--- check parameters
   if(!pipename || *pipename==0) return(false);
//--- close old
   Close();
//--- create named pipe
   m_handle=CreateNamedPipe(pipename,PIPE_ACCESS_DUPLEX,
                            PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
                            PIPE_UNLIMITED_INSTANCES,256*1024,256*1024,1000,NULL);

   if(m_handle==INVALID_HANDLE_VALUE)
     {
      wprintf(L"Creating pipe '%s' failed\n",pipename);
      return(false);
     }
//--- ok
   wprintf(L"Pipe '%s' created\n",pipename);
   return(true);
  }

要获得客户端连接,您需要使用 ConnectNamedPipe 函数:

//+------------------------------------------------------------------+
//| Connect client                                                   |
//+------------------------------------------------------------------+
bool CPipeManager::ConnectClient(void)
  {
//--- pipe exists?
   if(m_handle==INVALID_HANDLE_VALUE) return(false);
//--- connected?
   if(!m_connected)
     {
      //--- connect
      if(ConnectNamedPipe(m_handle,NULL)==0)
        {
         //--- client already connected before ConnectNamedPipe?
         if(GetLastError()!=ERROR_PIPE_CONNECTED)
            return(false);
         //--- ok
        }
      m_connected=true;
     }
//---
   return(true);
  }

数据交换使用 4 个简单函数实现:

  • CPipeManager::Send(void *data,size_t data_size)
  • CPipeManager::Read(void *data,size_t data_size)
  • CPipeManager::SendString(LPCSTR command)
  • CPipeManager::ReadString(LPSTR answer,size_t answer_maxlen)

它们允许您将数据作为二进制数据或 ANSI 文本字符串以 MQL5 兼容的模式发送/接收。此外,在默认情况下,由于 MQL5 中的 CFilePipe 以 ANSI 模式打开一个文件,字符串在接收和发送时自动转换为 Unicode。如果您的 MQL5 程序以 Unicode 模式 (FILE_UNICODE) 打开文件,则它可以交换 Unicode 字符串(具有 BOM 开始签名)。

客户端实施

我们将以 MQL5 编写我们的客户端。它将能够使用来自标准库的 CFilePipe 类执行常规文件操作。此类几乎与 CFileBin 相同,但它在读取数据前包含对虚拟文件中数据可用性的重要验证。

//+------------------------------------------------------------------+
//| Wait for incoming data                                           |
//+------------------------------------------------------------------+
bool CFilePipe::WaitForRead(const ulong size)
  {
//--- check handle and stop flag
   while(m_handle!=INVALID_HANDLE && !IsStopped())
     {
      //--- enough data?
      if(FileSize(m_handle)>=size)
         return(true);
      //--- wait a little
      Sleep(1);
     }
//--- failure
   return(false);
  }

//+------------------------------------------------------------------+
//| Read an array of variables of double type                        |
//+------------------------------------------------------------------+
uint CFilePipe::ReadDoubleArray(double &array[],const int start_item,const int items_count)
  {
//--- calculate size
   uint size=ArraySize(array);
   if(items_count!=WHOLE_ARRAY) size=items_count;
//--- check for data
   if(WaitForRead(size*sizeof(double)))
      return FileReadArray(m_handle,array,start_item,items_count);
//--- failure
   return(0);
  }

命名管道在实施本地模式和网络模式时具有显著差异。如果没有这样一个验证,网络模式操作在发送较大数据量时(超过 64K)将总是返回读取错误。

我们通过两个检查连接至服务器:连接至名为 "RemoteServerName" 的远程计算机或连接至本地机器。

void OnStart()
  {
//--- wait for pipe server
   while(!IsStopped())
     {
      if(ExtPipe.Open("\\\\RemoteServerName\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) break;
      if(ExtPipe.Open("\\\\.\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) break;
      Sleep(250);
     }
   Print("Client: pipe opened");

数据交换

在成功连接后,我们发送一个文本字符串和标识信息至服务器。由于文件以 ANSI 模式打开,Unicode 字符串将被自动转换为 ANSI。

//--- send welcome message
   if(!ExtPipe.WriteString(__FILE__+" on MQL5 build "+IntegerToString(__MQ5BUILD__)))
     {
      Print("Client: sending welcome message failed");
      return;
     }

作为答复,服务器将发送其字符串 "Hello from pipe server" 和整数 1234567890。客户端将再次发送字符串 "Test string" 和整数 1234567890。

//--- read data from server
   string        str;
   int           value=0;

   if(!ExtPipe.ReadString(str))
     {
      Print("Client: reading string failed");
      return;
     }
   Print("Server: ",str," received");

   if(!ExtPipe.ReadInteger(value))
     {
      Print("Client: reading integer failed");
      return;
     }
   Print("Server: ",value," received");
//--- send data to server
   if(!ExtPipe.WriteString("Test string"))
     {
      Print("Client: sending string failed");
      return;
     }

   if(!ExtPipe.WriteInteger(value))
     {
      Print("Client: sending integer failed");
      return;
     }

好了,我们完成了简单的数据交换。现在,是时候转到性能基准了。

性能基准

作为测试,我们将从服务器发送 8 兆字节块的双精度类型数字数组的千兆字节数据至客户端,然后检查数据块的正确性并测量传输速率。

下面是 C++ 服务器中的代码:

//--- benchmark
   double  volume=0.0;
   double *buffer=new double[1024*1024];   // 8 Mb

   wprintf(L"Server: start benchmark\n");
   if(buffer)
     {
      //--- fill the buffer
      for(size_t j=0;j<1024*1024;j++)
         buffer[j]=j;
      //--- send 8 Mb * 128 = 1024 Mb to client
      DWORD   ticks=GetTickCount();

      for(size_t i=0;i<128;i++)
        {
         //--- setup guard signatures
         buffer[0]=i;
         buffer[1024*1024-1]=i+1024*1024-1;
         //--- 
         if(!manager.Send(buffer,sizeof(double)*1024*1024))
           {
            wprintf(L"Server: benchmark failed, %d\n",GetLastError());
            break;
           }
         volume+=sizeof(double)*1024*1024;
         wprintf(L".");
        }
      wprintf(L"\n");
      //--- read confirmation
      if(!manager.Read(&value,sizeof(value)) || value!=12345)
         wprintf(L"Server: benchmark confirmation failed\n");
      //--- show statistics
      ticks=GetTickCount()-ticks;
      if(ticks>0)
         wprintf(L"Server: %.0lf Mb sent at %.0lf Mb per second\n",volume/1024/1024,volume/1024/ticks);
      //---
      delete[] buffer;
     }

以及 MQL5 客户端中的代码:

//--- benchmark
   double buffer[];
   double volume=0.0;

   if(ArrayResize(buffer,1024*1024,0)==1024*1024)
     {
      uint  ticks=GetTickCount();
      //--- read 8 Mb * 128 = 1024 Mb from server
      for(int i=0;i<128;i++)
        {
         uint items=ExtPipe.ReadDoubleArray(buffer);
         if(items!=1024*1024)
           {
            Print("Client: benchmark failed after ",volume/1024," Kb, ",items," items received");
            break;
           }
         //--- check the data
         if(buffer[0]!=i || buffer[1024*1024-1]!=i+1024*1024-1)
           {
            Print("Client: benchmark invalid content");
            break;
           }
         //---
         volume+=sizeof(double)*1024*1024;
        }
      //--- send confirmation
      value=12345;
      if(!ExtPipe.WriteInteger(value))
         Print("Client: benchmark confirmation failed ");
      //--- show statistics
      ticks=GetTickCount()-ticks;
      if(ticks>0)
         printf("Client: %.0lf Mb received at %.0lf Mb per second\n",volume/1024/1024,volume/1024/ticks);
      //---
      ArrayFree(buffer);
     }

注意,检查传输块的第一个和最后一个元素是为了确保传输期间未发生错误。同样,当传输完成,客户端发送数据成功接收的确认信号至服务器。如果您不使用最终确认,在一方过早地关闭连接的情况下您将很容易丢失数据。

本机运行 PipeServer.exe 服务器,并将 PipeClient.mq5 脚本附加至任意图表:

PipeServer.exe PipeClient.mq5
MQL5 Pipe Server
Copyright 2012, MetaQuotes Software Corp.
Pipe '\\.\pipe\MQL5.Pipe.Server' created
Client: waiting for connection...
Client: connected as 'PipeClient.mq5 on MQL5 build 705'
Server: send string
Server: send integer
Server: read string
Server: 'Test string' received
Server: read integer
Server: 1234567890 received
Server: start benchmark
......................................................
........
Server: 1024 Mb sent at 2921 Mb per second
PipeClient (EURUSD,H1)  Client: pipe opened
PipeClient (EURUSD,H1)  Server: Hello from pipe server received
PipeClient (EURUSD,H1)  Server: 1234567890 received
PipeClient (EURUSD,H1)  Client: 1024 Mb received at 2921 Mb per second

对于本地交换,传输速率相当惊人 - 几乎 3 千兆字节每秒。这意味着命名管道可用于传输几乎任意的数据量至 MQL5 程序。

现在,我们在一个普通的千兆局域网中测试数据传输性能:

PipeServer.exe PipeClient.mq5
MQL5 Pipe Server
Copyright 2012, MetaQuotes Software Corp.
Pipe '\\.\pipe\MQL5.Pipe.Server' created
Client: waiting for connection...
Client: connected as 'PipeClient.mq5 on MQL5 build 705'
Server: send string
Server: send integer
Server: read string
Server: 'Test string' received
Server: read integer
Server: 1234567890 received
Server: start benchmark
......................................................
........
Server: 1024 Mb sent at 63 Mb per second
PipeClient (EURUSD,H1)  Client: pipe opened
PipeClient (EURUSD,H1)  Server: Hello from pipe server received
PipeClient (EURUSD,H1)  Server: 1234567890 received
PipeClient (EURUSD,H1)  Client: 1024 Mb received at 63 Mb per second

在本地网络中,千兆字节数据以 63 兆字节每秒的速率传输,这相当不错。事实上,这是千兆网络最大带宽的 63%。

总结

MetaTrader 5 交易平台的保护系统不允许 MQL5 程序在外部运行它们的沙箱,从而保护交易人员在使用不受信的“EA 交易”时免受威胁。使用命名管道,您可以轻松创建与第三方软件的整合和从外部管理 EA。且安全无虞。

全部回复

0/140

达人推荐

量化课程

    移动端课程