Comunicándonos con Meta Trader 5 usando conexiones designadas sin utilizar DLL
MetaQuotes | 8 mayo, 2014
Introducción
Muchos desarrolladores se enfrentan con el mismo problema: cómo llegar al módulo del terminal sin utilizar DLL poco seguras.
Uno de los métodos más sencillos y seguros es utilizar conexiones designadas que funcionan como operaciones de archivo normales. Estos nos permiten organizar la comunicación cliente-servidor entre procesadores entre los programas. Aunque ya hay un artículo publicado sobre este tema (Una solución libre de DLL para la comunicación entre los terminales de Meta Trader 5 usando conexiones designadas) que muestra cómo habilitar el acceso a las DLL, usaremos las características estándar y seguras del terminal de cliente.
Puede encontrar más información sobre las conexiones designadas en la librería MSDN, pero nos iremos a ejemplos prácticos en C++ y MQL5. Implementaremos un servidor, un cliente y un intercambio de datos entre los mismos y luego calificaremos el rendimiento.
Implementación del servidor
Vamos a crear el código de un simple servidor en C++. Un script del terminal conectará con este servidor e intercambiará con datos con él. El núcleo del servidor tiene el siguiente conjunto de funciones de WinAPI:
- CreateNamedPipe - crea un conexión designada.
- ConnectNamedPipe - permite que el servidor espere la conexiones del cliente.
- WriteFile - escribe datos en una conexión.
- ReadFile - lee datos de una conexión.
- FlushFileBuffers - limpia buffers acumulados.
- DisconnectNamedPipe - desconecta el servidor.
- CloseHandle - cierra el controlador.
Una vez que se abre una conexión designada esta devuelve un controlador de archivo que puede usarse para operaciones de lectura/escritura de archivos habituales. Como resultado, tenemos un mecanismo muy simple que no requiere ningún conocimiento especial en operaciones de red.
Las conexiones designadas tienen una característica distintiva: pueden ser locales y de red. Es decir, es fácil implementar un servidor remoto que acepte conexiones de red de los terminales de cliente.
Este es un simple ejemplo de creación de un servidor local como canal full-duplex que funciona en modo de intercambio de bytes:
//--- 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); }
Para obtener una conexión de cliente tenemos que usar la función 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); }
El intercambio de datos se organiza usando 4 funciones simples:
- 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)
Estas nos permiten enviar/recibir datos binarios o como cadenas de texto ANSI en modo compatible con MQL5. Además, como CFilePipe en MQL5 abre un archivo en modo ANSI por defecto, los strings son convertidos automáticamente en formato Unicode al recibir y enviar. Si su programa MQL5 abre un archivo en modo Unicode (FILE_UNICODE), entonces puede intercambiar strings Unicode (con la firma inicial BOM).
Implementación del cliente
Escribiremos nuestro cliente en MQL5. Este podrá realizar operaciones de archivo de forma habitual usando la clase CFilePipe de la librería estándar. Esta clase es casi idéntica a CFileBin, pero contiene una importante verificación de la disponibilidad de los datos en un archivo virtual antes de leerlos.
//+------------------------------------------------------------------+ //| 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); }
Las conexiones designadas tienen diferencias importantes en la implementación de sus modos local y en red. Con dicha verificación, las operaciones en modo de red siempre devolverán un error de lectura al enviar grandes cantidades de datos (más de 64 K).
Vamos a conectar con el servidor con dos comprobaciones: bien a un ordenador remoto llamado 'RemoteServerName' o a una máquina local.
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");
Intercambio de datos
Después de la conexión con éxito vamos a enviar una cadena de texto con la información de identificación al servidor. La cadena de Unicode será convertida de forma automática a ANSI, ya que el archivo se ha abierto en modo ANSI.
//--- send welcome message if(!ExtPipe.WriteString(__FILE__+" on MQL5 build "+IntegerToString(__MQ5BUILD__))) { Print("Client: sending welcome message failed"); return; }
Como respuesta, el servidor enviará su string "Hello from pipe server" y el entero 1234567890. Una vez más el cliente enviará el string "Test string" y el entero 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; }
Ok, hemos terminado con el intercambio de datos simple. Ahora es el momento de probar el rendimiento.
Prueba del rendimiento
Como prueba que es, enviaremos 1 gigabyte de datos como matriz de tipo doble en bloques de 8 megabytes desde el servidor al cliente, y luego comprobaremos si los bloques son correctos y mediremos el ratio de transferencia.
Este es el código en el servidor de 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; }
y en el cliente 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); }
Observe que los elementos primero y último de los bloques transferidos se comprueban para garantizar que no hay errores durante la transferencia. Además, cuando se ha completado la transferencia el cliente envía una señal de confirmación al servidor sobre la correcta recepción de los datos. Si no quiere utilizar confirmaciones finales, encontrará fácilmente una pérdida de datos si una de las partes cierra la conexión demasiado pronto.
Ejecute el servidor PipeServer.exe localmente y adjunte el script PipeClient.mq5 a cualquier gráfico:
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 |
Para un intercambio local, la velocidad de transferencia es realmente increíble: casi 3 gigabytes por segundo. Esto significa que las conexiones designadas puede utilizarse para trasferir casi cualquier cantidad de datos en los programas de MQL5.
Ahora vamos a medir el rendimiento de transferencia de datos en una red LAN ordinaria de 1 gigabyte:
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 |
En una red local, se ha transferido un gigabyte de datos a una velocidad de 63 megabytes por segundo, lo que está muy bien. De hecho, es un 63% del máximo ancho de banda de la red local.
Conclusión
El sistema de protección de la plataforma de trading Meta Trader 5 no permite que los programas MQL5 se ejecuten fuera del sandbox, protegiendo a los operadores frente a amenazas al usar asesores expertos no fiables. El uso de conexiones designadas permite crear integraciones con software de terceras partes con facilidad y gestionar los asesores expertos desde el exterior. De forma segura.