Trabajando con sockets en MQL, o Cómo convertirse en proveedor de señales

--- | 1 agosto, 2016

Un poco de patetismo

Los sockets... ¿Qué podría existir sin ellos en este mundo de información? Aparecieron por primera vez en 1982 y prácticamente no han cambiado hasta el día de hoy, siguen funcionando para nosotros cada segundo. Son la base de una red, las terminaciones nerviosas del Matrix en el que vivimos.

En cuanto enciende por la mañana el terminal MetaTrader, este crea de inmediato los sockets y se conecta a los servidores. Usted abre su navegador y decenas de conexiones de socket se crean y desaparecen para hacerle llegar información desde la web, para el envío de correos, señales de tiempo exacto, gigabytes de cálculos distributivos.

Bien, para comenzar, tenemos que leer un poco de teoría. Eche un vistazo en Wiki  o en MSDN. En los artículos correspondientes se describe muy bien el arsenal completo de estructuras y funciones imprescindibles, y también se muestran ejemplos sobre la creación del servidor y el cliente.

En este artículo nos ocuparemos del traslado de estos conocimientos a MQL.

1. Portando las estructuras y funciones desde WinAPI

No es un secreto que WinAPI ha sido diseñado para el lenguaje C. Y el lenguaje MQL es prácticamente su hermano (tanto por espíritu como por su estilo de trabajo). Vamos a crear un archivo mqh para estas funciones WinAPI, que usaremos en el programa MQL principal. La secuencia de nuestras acciones consistirá en proceder al traslado según lo necesitimos.

Para un cliente de TCP solo necesitaremos varias funciones:

Para el servidor TCP son necesarias funciones análogas, a no ser que nos enlacemos a un puerto concreto y pasemos el socket al modo de espera de conexión. En conclusión:

En el caso del socket UDP, serán menos los pasos (prácticamente no hay "apretón de manos" entre servidor y cliente). cliente UDP:

en el servidor UDP se añade solo una función bind:

Como puede ver, el camino no es demasiado complejo, pero para llamar a cada función necesitaremos rellenar las estructuras.


a) WSAStartup()

Veamos su descripción en MSDN:
WINAPI:
int WSAAPI WSAStartup(_In_ WORD wVersionRequested,  _Out_ LPWSADATA lpWSAData);
_In_, _Out_ — son directivas "define" vacías que indican el área de aplicación del parámetro. WSAAPI – describe las normas de transmisión de parámetros, pero en nuestro caso, podemos dejarlas vacías.

Como se puede ver por la documentación, necesitaremos también el macro MAKEWORD para especificar la versión necesaria en el primer parámetro, así como el puntero a la estructura LPWSADATA. El asunto del macro no es complicado, lo copiamos del archivo de encabezamiento:

#define MAKEWORD(a, b)      ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
Además, todos los tipos de datos se describen con facilidad en el marco de MQL:
#define BYTE         uchar
#define WORD         ushort
#define DWORD        int
#define DWORD_PTR    ulong
La estructura WSADATA la copiamos de MSDN. El nombre de la mayoría de los tipos de datos lo dejaremos, para que sea más cómodo de leer, tanto más que ya los tenemos definidos más arriba.
struct WSAData
{
  WORD wVersion;
  WORD  wHighVersion;
  char szDescription[WSADESCRIPTION_LEN+1];
  char szSystemStatus[WSASYS_STATUS_LEN+1];
  ushort iMaxSockets;
  ushort iMaxUdpDg;
  char  lpVendorInfo[];
}
Preste atención a que el último parámetro lpVendorInfo en MQL ha sido descrito como matriz (en C era un puntero a char*). Las constantes para el tamaño de las matrices también las introducimos en las "define". Al final, el puntero a la estructura lo describiremos como:
#define LPWSADATA        char&

¿Por qué es así? Es muy sencillo. Cualquier estructura no es otra cosa que un trozo de memoria limitado. Este trozo se puede representar de cualquier forma, por ejemplo, en forma de otra estructura del mismo tamaño, o como una matriz de tamaño análogo. Yo uso la presentación en forma de matriz, por eso el tipo char& en todas las funciones será la dirección de la matriz cuyo tamaño corresponda al tamaño de la estructura necesaria. La declaración resultante de la función en MQL tiene el siguiente aspecto:

MQL:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData[]);
Y este es el aspecto que tiene la llamada de la función y la recepción del resultado en la estructura WSAData:
char wsaData[]; // matriz de bytes de la futura estructura
ArrayResize(wsaData, sizeof(WSAData)); // cambiamos su tamaño al tamaño de la estructura
WSAStartup(MAKEWORD(2,2), wsaData); // llamamos a la función

Los datos llegarán a la matriz de bytes wsaData, de la cual podemos tomar fácilmente la información usando la conversión de tipos.

Espero que esta parte no le haya parecido demasiado complicada, solo se trata de la primera función, y ya hay que realizar tanto trabajo. Pero ahora nos resulta comprensible el principio básico, por eso en lo sucesivo todo será más sencillo e interesante.

b) socket()

WINAPI:
SOCKET WSAAPI socket(_In_ int af, _In_ int type, _In_ int protocol);

Procedemos de manera análoga, copiamos los datos desde MSDN.

Dado que usamos sockets TCP para IPv4, entonces pondremos directamente las constantes para los parámetros de esta función:

#define SOCKET           uint
#define INVALID_SOCKET   (SOCKET)(~0)
#define SOCKET_ERROR            (-1)
#define NO_ERROR         0
#define AF_INET          2 // internetwork: UDP, TCP, etc.
#define SOCK_STREAM      1
#define IPPROTO_TCP      6

c) ioctlsocket()

MQL:
int ioctlsocket(SOCKET s, int cmd, int &argp);

En ella, el último argumento ha sido cambiado de un puntero a una dirección:

d) connect()

WINAPI:
int connect(_In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen);

La transmisión de la estructura sockaddr supone una pequeña dificultad, pero ya conocemos el principio básico: cambiamos las estructuras por matrices de bytes y las usamos para transmitir datos a las funciones WinAPI.

Tomamos de MSDN la propia estructura sin cambios:

struct sockaddr
{
    ushort sa_family; // Address family.
    char sa_data[14]; // Up to 14 bytes of direct address.
};
Como acordamos, el puntero a ella se implementará a través de la dirección de la matriz:
#define LPSOCKADDR    char&
En los ejemplos, MSDN usa la estructura sockaddr_in. Es análoga en cuanto a sus dimensiones, pero los parámetros están escritos de forma diferente:
struct sockaddr_in
{
    short   sin_family;
    ushort sin_port;
    struct  in_addr sin_addr;
    char    sin_zero[8];
};
Los datos para sin_addr  son "union", una de cuyas representaciones es un entero de ocho bytes:
struct in_addr
{
   ulong s_addr;
};
En resumen, la descripción de la función en MQL tiene este aspecto:
MQL:
int connect(SOCKET s, LPSOCKADDR name[], int namelen);

En esta etapa, estamos completamente preparados para crear un socket de cliente. Solo nos queda un poco: las funciones de recepción y transmisión de datos.

e) recv()  y send() para TCP

Los prototipos tienen este aspecto:

WINAPI:
int send(_In_ SOCKET s, _In_ const char* buf, _In_ int len, _In_ int flags);
int recv(_In_  SOCKET s, _Out_     char* buf, _In_ int len, _In_ int flags);
MQL:
int send(SOCKET s, char& buf[], int len, int flags);
int recv(SOCKET s, char& buf[], int len, int flags);
Como se puede ver, el segundo parámetro ha sido cambiado del puntero char* a la matriz char& []

f) recvfrom()  y sendto() para UPD

Los prototipos en MQL tienen este aspecto:

WINAPI:
int recvfrom(_In_  SOCKET s, _Out_ char* buf, _In_ int len, _In_ int flags, _Out_  struct sockaddr *from,
  _Inout_opt_ int *fromlen);
int sendto(_In_ SOCKET s, _In_ const char* buf, _In_ int len, _In_ int flags,  _In_ const struct sockaddr *to,
  _In_       int tolen);
MQL:
int recvfrom(SOCKET s,char &buf[],int len,int flags,LPSOCKADDR from[],int &fromlen);
int sendto(SOCKET s,const char &buf[],int len,int flags,LPSOCKADDR to[],int tolen);

Y para finalizar, dos funciones importantes para limpiar y cerrar los manejadores:

g) closesocket() y WSACleanup()

MQL:
int closesocket(SOCKET s);
int WSACleanup();

Archivo final de las funciones WinAPI portadas:
#define BYTE              uchar
#define WORD              ushort
#define DWORD             int
#define DWORD_PTR         ulong
#define SOCKET            uint

#define MAKEWORD(a, b)      ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))

#define WSADESCRIPTION_LEN      256
#define WSASYS_STATUS_LEN       128

#define INVALID_SOCKET  (SOCKET)(~0)
#define SOCKET_ERROR    (-1)
#define NO_ERROR        0
#define SOMAXCONN       128

#define AF_INET         2 // internetwork: UDP, TCP, etc.
#define SOCK_STREAM     1
#define IPPROTO_TCP     6

#define SD_RECEIVE      0x00
#define SD_SEND         0x01
#define SD_BOTH         0x02

#define IOCPARM_MASK    0x7f            /* parameters must be < 128 bytes */
#define IOC_IN          0x80000000      /* copy in parameters */
#define _IOW(x,y,t)     (IOC_IN|(((int)sizeof(t)&IOCPARM_MASK)<<16)|((x)<<8)|(y))
#define FIONBIO         _IOW('f', 126, int) /* set/clear non-blocking i/o */
//------------------------------------------------------------------    struct WSAData
struct WSAData
  {
   WORD              wVersion;
   WORD              wHighVersion;
   char              szDescription[WSADESCRIPTION_LEN+1];
   char              szSystemStatus[WSASYS_STATUS_LEN+1];
   ushort            iMaxSockets;
   ushort            iMaxUdpDg;
   char              lpVendorInfo[];
  };

#define LPWSADATA               char&
//------------------------------------------------------------------    struct sockaddr_in
struct sockaddr_in
  {
   ushort            sin_family;
   ushort            sin_port;
   ulong             sin_addr; //struct in_addr { ulong s_addr; };
   char              sin_zero[8];
  };
//------------------------------------------------------------------    struct sockaddr
struct sockaddr
  {
   ushort            sa_family; // Address family.
   char              sa_data[14]; // Up to 14 bytes of direct address.
  };
#define LPSOCKADDR      char&

struct ref_sockaddr { char ref[2+14]; };

//------------------------------------------------------------------    import Ws2_32.dll
#import "Ws2_32.dll"
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData[]);
int WSACleanup();
int WSAGetLastError();

ushort htons(ushort hostshort);
ulong inet_addr(char& cp[]);
string inet_ntop(int Family,ulong &pAddr,char &pStringBuf[],uint StringBufSize);
ushort ntohs(ushort netshort);

SOCKET socket(int af,int type,int protocol);
int ioctlsocket(SOCKET s,int cmd,int &argp);
int shutdown(SOCKET s,int how);
int closesocket(SOCKET s);

// funciones de servidor
int bind(SOCKET s,LPSOCKADDR name[],int namelen);
int listen(SOCKET s,int backlog);
SOCKET accept(SOCKET s,LPSOCKADDR addr[],int &addrlen);

// funciones de cliente
int connect(SOCKET s,LPSOCKADDR name[],int namelen);
int send(SOCKET s,char &buf[],int len,int flags);
int recv(SOCKET s,char &buf[],int len,int flags);

#import


2. Creación del cliente y el servidor

Después de pensar un tiempo sobre la forma en la que trabajar con un socket para posteriores experimentos, he elegido una demostración del trabajo con sus funciones sin clases. En primer lugar, esto nos dará una idea rápida de que nos encontramos aquí simplemente ante programación lineal no ramificada. En segundo lugar, esto permite realizar la refactorización de la función conforme a nuestras necesidades, conviertiéndola a cualquier ideología de POO que usted tenga. Sé por propia experiencia que los programadores escrutan las clases simples en profundidad hasta sus cimientos para comprender cómo funcionan.

¡Importante! No olvide en todos sus experimentos que el puerto enlazado no se libera automáticamente al abortar el código del servidor. Esto provocará que la nueva creación del socket y el intento de llamar a "bind" incurran en el error: Address already in use. Para resolver este problema es necesario o bien usar en el socket la opción SO_REUSEADDR, o bien (lo que es más sencillo) reiniciar el terminal. Use las utilidades de monitoreo, por ejemplo, TCPViewer, para realizar un seguimiento de los sockets creados en su SO.

Asimismo, debe entender que el cliente puede conectarse al servidor con la condición de que el servidor no esté oculto detrás de NAT o de que el puerto para el cliente/servidor no esté bloqueado en el SO o router.

Por eso, usted puede experimentar primero con el servidor y el cliente de forma local en una computadora. Pero para que funcione totalmente con multitud de clientes, el servidor debe ser iniciado como mínimo en un VPS con una IP externa "blanca" y un puerto abierto utilizable al exterior.

Ejemplo 1. Envío de etiquetado del gráfico a los clientes

Vamos a comenzar con una interacción sencilla: la transmisión única a un cliente de un archivo tpl desde el servidor.

En este caso, no hay necesidad de mantener el ciclo send/recv del cliente, ya que necesitamos recibir solo una porción de datos al conectarnos y luego cortar la conexión. Además, el corte de conexión lo llevará a cabo el servidor justo después del envío de datos.

Es decir, el servidor al conectarse al mismo el cliente, hace una llamada Send y finaliza el socket, y el cliente en este momento envía Recv y finaliza el socket de manera análoga. Está claro que en casos más interesantes se puede hacer una transmisión permanente de cambio del gráfico, digamos una sincronización momentánea del gráfico del cliente y el servidor. Esto resultaría útil para los gurús del trading, que pueden enseñar a sus jóvenes padawan los gráficos online. Pero a día de hoy, esto se realiza con la transmisión del flujo de vídeo desde la pantalla a través del software de webinar o skype. Por eso vamos a dejar este tema para el foro.

¿A quién y en qué circunstancias le será útil este ejemplo de código? Por ejemplo, usted coloca sus indicadores u objetos gráficos en el gráfico a diario, o cada hora, o cada minuto (subraye lo que proceda). Además, en un gráfico aparte usted tiene un servidor-experto que escucha las conexiones de los clientes y les da el tpl actual del símbolo y el periodo que necesitan.

Los clientes satisfechos ahora estarán al tanto de los objetivos y las señales comerciales de boca de usted. Les será suficiente con iniciar periódicamente un script que descargue del servidor tlp y que lo use en el gráfico.

Y bien, vamos a comenzar por el servidor. Todo funciona en el evento OnTimer, que juega el papel de función de "hilo" del experto. Una vez por segundo se comprueban los bloques clave del servidor: Espera del cliente -> Envío de datos al cliente -> Cierre de la conexión. Asimismo, se comprueba la actividad del propio socket del servidor y, en caso de corte, se crea de nuevo el socket.

Por desgracia, la plantilla tlp guardada no estará disponible desde el sandbox del archivo. Por eso, para tomarlo de la carpeta Profiles\Templates, será necesario usar de nuevo una WinAPI. No voy a describir con detalle esta parte, podrá ver el listado completo más abajo.

//+------------------------------------------------------------------+
//|                                                        TplServer |
//|                       programming & development - Alexey Sergeev |
//+------------------------------------------------------------------+
#property copyright "© 2006-2016 Alexey Sergeev"
#property link      "profy.mql@gmail.com"
#property version   "1.00"

#include "SocketLib.mqh"

input string Host="0.0.0.0";
input ushort Port=8080;

uchar tpl[];
int iCnt=0;
string exname="";
SOCKET server=INVALID_SOCKET;
//------------------------------------------------------------------    OnInit
int OnInit()
  {
   EventSetTimer(1);
   exname=MQLInfoString(MQL_PROGRAM_NAME)+".ex5";
   return 0;
  }
//------------------------------------------------------------------    OnDeinit
void OnDeinit(const int reason)
  {
   EventKillTimer();
   CloseClean();
  }
//------------------------------------------------------------------    OnChartEvent
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(iCnt==0) // limitación en la creación del archivo de la plantilla - no más de una vez por segundo
     {
      Print("Create TPL");
      uchar buf[];
      CreateTpl(buf);
      uchar smb[]; StringToCharArray(Symbol(),smb); ArrayResize(smb,10);
      uchar tf[]; StringToCharArray(IntegerToString(Period()),tf); ArrayResize(tf,10);

      // creamos los datos a enviar
      ArrayCopy(tpl,smb, ArraySize(tpl)); // añadimos el nombre del símbolo
      ArrayCopy(tpl, tf, ArraySize(tpl)); // añadimos el valor del periodo
      ArrayCopy(tpl,buf, ArraySize(tpl)); // añadimos la propia plantilla
     }
   iCnt++;
  }
//------------------------------------------------------------------    OnTimer
void OnTimer()
  {
   iCnt=0; // reseteamos el contador de creación de plantillas

   if(server==INVALID_SOCKET)
      StartServer(Host,Port);
   else
     {
      // en el ciclo obtenemos todos los clientes y enviamos a cada uno la plantilla actual del gráfico
      SOCKET client=INVALID_SOCKET;
      do
        {
         client=AcceptClient(); // Accept a client socket
         if(client==INVALID_SOCKET) return;

         int slen=ArraySize(tpl);
         int res=send(client,tpl,slen,0);
         if(res==SOCKET_ERROR) Print("-Send failed error: "+WSAErrorDescript(WSAGetLastError()));
         else printf("Sent %d bytes of %d",res,slen);

         if(shutdown(client,SD_BOTH)==SOCKET_ERROR) Print("-Shutdown failed error: "+WSAErrorDescript(WSAGetLastError()));
         closesocket(client);
        }
      while(client!=INVALID_SOCKET);
     }
  }
//------------------------------------------------------------------    StartServer
void StartServer(string addr,ushort port)
  {
// inicializamos la biblioteca
   char wsaData[]; ArrayResize(wsaData,sizeof(WSAData));
   int res=WSAStartup(MAKEWORD(2,2), wsaData);
   if(res!=0) { Print("-WSAStartup failed error: "+string(res)); return; }

// creamos el socket
   server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
   if(server==INVALID_SOCKET) { Print("-Create failed error: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

// enlazamos con la dirección y el puerto
   Print("try bind..."+addr+":"+string(port));

   char ch[]; StringToCharArray(addr,ch);
   sockaddr_in addrin;
   addrin.sin_family=AF_INET;
   addrin.sin_addr=inet_addr(ch);
   addrin.sin_port=htons(port);
   ref_sockaddr ref=(ref_sockaddr)addrin;
   if(bind(server,ref.ref,sizeof(addrin))==SOCKET_ERROR)
     {
      int err=WSAGetLastError();
      if(err!=WSAEISCONN) { Print("-Connect failed error: "+WSAErrorDescript(err)+". Cleanup socket"); CloseClean(); return; }
     }

// activamos el modo sin bloqueo
   int non_block=1;
   res=ioctlsocket(server,(int)FIONBIO,non_block);
   if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); CloseClean(); return; }

// escuchamos el puerto y aceptamos las conexiones de cliente
   if(listen(server,SOMAXCONN)==SOCKET_ERROR) { Print("Listen failed with error: ",WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

   Print("start server ok");
  }
//------------------------------------------------------------------    Accept
SOCKET AcceptClient() // Accept a client socket
  {
   if(server==INVALID_SOCKET) return INVALID_SOCKET;
   ref_sockaddr ch;
   int len=sizeof(ref_sockaddr);
   SOCKET new_sock=accept(server,ch.ref,len);
//sockaddr_in aclient=(sockaddr_in)ch; transformamos en una estructura, si es necesario recibir información adicional
   if(new_sock==INVALID_SOCKET)
     {
      int err=WSAGetLastError();
      if(err==WSAEWOULDBLOCK) Comment("\nWAITING CLIENT ("+string(TimeCurrent())+")");
      else { Print("Accept failed with error: ",WSAErrorDescript(err)); CloseClean(); return INVALID_SOCKET; }
     }
   return new_sock;
  }
//------------------------------------------------------------------    CloseClean
void CloseClean() // close socket
  {
   if(server!=INVALID_SOCKET) { closesocket(server); server=INVALID_SOCKET; }
   WSACleanup();
   Print("stop server");
  }

//------------------------------------------------------------------
#import "kernel32.dll"
int CreateFileW(string lpFileName,uint dwDesiredAccess,uint dwShareMode,uint lpSecurityAttributes,uint dwCreationDisposition,uint dwFlagsAndAttributes,int hTemplateFile);
bool ReadFile(int h,ushort &lpBuffer[],uint nNumberOfBytesToRead,uint &lpNumberOfBytesRead,int lpOverlapped=0);
uint SetFilePointer(int h,int lDistanceToMove,int,uint dwMoveMethod);
bool CloseHandle(int h);
uint GetFileSize(int h,int);
#import

#define FILE_BEGIN                              0
#define OPEN_EXISTING                   3
#define GENERIC_READ                    0x80000000
#define FILE_ATTRIBUTE_NORMAL           0x00000080
#define FILE_SHARE_READ_         0x00000001  
//------------------------------------------------------------------    LoadTpl
bool CreateTpl(uchar &abuf[])
  {
   string path=TerminalInfoString(TERMINAL_PATH);
   string name="tcpsend.tpl";

// creamos una plantilla
   ChartSaveTemplate(0,name);

// leemos la plantilla en la matriz
   path+="\\Profiles\\Templates\\"+name;
   int h=CreateFileW(path, GENERIC_READ, FILE_SHARE_READ_, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
   if(h==INVALID_HANDLE) return false;
   uint sz=GetFileSize(h,NULL);
   ushort rbuf[];
   ArrayResize(rbuf,sz); ArrayInitialize(rbuf,0);
   SetFilePointer(h,0,NULL,FILE_BEGIN); // nos movemos hasta el principio
   int r; ReadFile(h,rbuf,sz,r,NULL);
   CloseHandle(h);

// quitamos de la plantilla el nombre del experto
   string a=ShortArrayToString(rbuf);
   ArrayResize(rbuf,0);
   StringReplace(a,exname," ");
   StringToShortArray(a,rbuf);

// copiamos el archivo en la matriz de bytes (conservando Unicode)
   sz=ArraySize(rbuf);
   ArrayResize(abuf,sz*2);
   for(uint i=0; i<sz;++i) { abuf[2*i]=(uchar)rbuf[i]; abuf[2*i+1]=(uchar)(rbuf[i]>>8); }

   return true;
  }


El código del cliente es un poco más sencillo. Puesto que ya hemos decidido que se tratará de una única recepción de un archivo, entonces no necesitaremos un experto en marcha con un socket activo.

El cliente se implementa como script. Todo sucede en el evento OnStart.

//+------------------------------------------------------------------+
//|                                                        TplClient |
//|                       programming & development - Alexey Sergeev |
//+------------------------------------------------------------------+
#property copyright "© 2006-2016 Alexey Sergeev"
#property link      "profy.mql@gmail.com"
#property version   "1.00"

#include "..\Experts\SocketLib.mqh"

input string Host="127.0.0.1";
input ushort Port=8080;
SOCKET client=INVALID_SOCKET;
//------------------------------------------------------------------    OnStart
void OnStart()
  {
// inicializamos la biblioteca
   char wsaData[]; ArrayResize(wsaData,sizeof(WSAData));
   int res=WSAStartup(MAKEWORD(2,2), wsaData);
   if(res!=0) { Print("-WSAStartup failed error: "+string(res)); return; }

// creamos el socket
   client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
   if(client==INVALID_SOCKET) { Print("-Create failed error: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

// nos conectamos al servidor
   char ch[]; StringToCharArray(Host,ch);
   sockaddr_in addrin;
   addrin.sin_family=AF_INET;
   addrin.sin_addr=inet_addr(ch);
   addrin.sin_port=htons(Port);

   ref_sockaddr ref=(ref_sockaddr)addrin;
   res=connect(client,ref.ref,sizeof(addrin));
   if(res==SOCKET_ERROR)
     {
      int err=WSAGetLastError();
      if(err!=WSAEISCONN) { Print("-Connect failed error: "+WSAErrorDescript(err)); CloseClean(); return; }
     }

// activamos el modo sin bloqueo
   int non_block=1;
   res=ioctlsocket(client,(int)FIONBIO,non_block);
   if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); CloseClean(); return; }

   Print("connect OK");

// recibimos los datos
   uchar rdata[];
   char rbuf[512]; int rlen=512; int rall=0; bool bNext=false;
   while(true)
     {
      res=recv(client,rbuf,rlen,0);
      if(res<0)
        {
         int err=WSAGetLastError();
         if(err!=WSAEWOULDBLOCK) { Print("-Receive failed error: "+string(err)+" "+WSAErrorDescript(err)); CloseClean(); return; }
        }
      else if(res==0 && rall==0) { Print("-Receive. connection closed"); break; }
      else if(res>0) { rall+=res; ArrayCopy(rdata,rbuf,ArraySize(rdata),0,res); }

      if(res>=0 && res<rlen) break;
     }

// cerramos el socket
   CloseClean();

   printf("receive %d bytes",ArraySize(rdata));

// tomamos el símbolo y el periodo del archivo
   string smb=CharArrayToString(rdata,0,10);
   string tf=CharArrayToString(rdata,10,10);

// guardamos el archivo de la plantilla
   int h=FileOpen("tcprecv.tpl", FILE_WRITE|FILE_SHARE_WRITE|FILE_BIN); if(h<=0) return;
   FileWriteArray(h,rdata,20);
   FileClose(h);

// aplicamos en el gráfico
   ChartSetSymbolPeriod(0,smb,(ENUM_TIMEFRAMES)StringToInteger(tf));
   ChartApplyTemplate(0,"\\Files\\tcprecv.tpl");
  }
//------------------------------------------------------------------    CloseClean
void CloseClean() // close socket
  {
   if(client!=INVALID_SOCKET)
     {
      if(shutdown(client,SD_BOTH)==SOCKET_ERROR) Print("-Shutdown failed error: "+WSAErrorDescript(WSAGetLastError()));
      closesocket(client); client=INVALID_SOCKET;
     }
   WSACleanup();
   Print("connect closed");
  }


Demostración de estos códigos operando entre sí:


Un lector atento notará que el socket de cliente se puede sustituir sin problema por la llamada de la función MQL WebRequest. Para ello, hay que añadir al servidor solo un par de líneas de encabezamientos HTTP, y añadir en el terminal de cliente en los ajustes una dirección-web ampliada. Puede experimentar por su cuenta con ello.

¡Importante! En algunos casos, se nota cierto comportamiento en el terminal: al llamar a la función WSACleanup MetaTrader cierra sus propias conexiones.

Si se encuentra con semejante problema durante sus experimentos, deje los comentarios correspondientes sobre WSAStartup y WSACleanup en el código.


Ejemplo 2. Sincronización del comercio según el símbolo

En este ejemplo, el servidor ya no cortará la conexión al enviar la información. La conexión del cliente se mantendrá estable. Además, los datos sobre cualquier cambio en el comercio en el servidor se enviarán de inmediato a través de los sockets de cliente. A su vez, el cliente que recibe el nuevo paquete de datos, sincroniza de inmediato su posición con la posición que llega del servidor.

Vamos a tomar como base el código del servidor y del cliente del ejemplo anterior. Añadiremos las funciones para trabajar con las posiciones.

Vamos a comenzar con el servidor:

//+------------------------------------------------------------------+
//|                                                     SignalServer |
//|                       programming & development - Alexey Sergeev |
//+------------------------------------------------------------------+
#property copyright "© 2006-2016 Alexey Sergeev"
#property link      "profy.mql@gmail.com"
#property version   "1.00"

#include "SocketLib.mqh"

input string Host="0.0.0.0";
input ushort Port=8081;

bool bChangeTrades;
uchar data[];
SOCKET server=INVALID_SOCKET;
SOCKET conns[];

//------------------------------------------------------------------    OnInit
int OnInit() { OnTrade(); EventSetTimer(1); return 0; }
//------------------------------------------------------------------    OnDeinit
void OnDeinit(const int reason) { EventKillTimer(); CloseClean(); }
//------------------------------------------------------------------    OnTrade
void OnTrade()
  {
   double lot=GetSymbolLot(Symbol());
   StringToCharArray("<<"+Symbol()+"|"+DoubleToString(lot,2)+">>",data); // transformamos la línea en una matriz de bytes
   bChangeTrades=true;
  }
//------------------------------------------------------------------    OnTimer
void OnTimer()
  {
   if(server==INVALID_SOCKET) StartServer(Host,Port);
   else
     {
      AcceptClients(); // añadimos a los clientes en espera
      if(bChangeTrades)
        {
         Print("send new posinfo to clients");
         Send(); bChangeTrades=false;
        }
     }
  }
//------------------------------------------------------------------    StartServer
void StartServer(string addr,ushort port)
  {
// inicializamos la biblioteca
   char wsaData[]; ArrayResize(wsaData,sizeof(WSAData));
   int res=WSAStartup(MAKEWORD(2,2), wsaData);
   if(res!=0) { Print("-WSAStartup failed error: "+string(res)); return; }

// creamos el socket
   server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
   if(server==INVALID_SOCKET) { Print("-Create failed error: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

// enlazamos con la dirección y el puerto
   Print("try bind..."+addr+":"+string(port));

   char ch[]; StringToCharArray(addr,ch);
   sockaddr_in addrin;
   addrin.sin_family=AF_INET;
   addrin.sin_addr=inet_addr(ch);
   addrin.sin_port=htons(port);
   ref_sockaddr ref=(ref_sockaddr)addrin;
   if(bind(server,ref.ref,sizeof(addrin))==SOCKET_ERROR)
     {
      int err=WSAGetLastError();
      if(err!=WSAEISCONN) { Print("-Connect failed error: "+WSAErrorDescript(err)+". Cleanup socket"); CloseClean(); return; }
     }

// activamos el modo sin bloqueo
   int non_block=1;
   res=ioctlsocket(server,(int)FIONBIO,non_block);
   if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); CloseClean(); return; }

// escuchamos el puerto y aceptamos las conexiones de cliente
   if(listen(server,SOMAXCONN)==SOCKET_ERROR) { Print("Listen failed with error: ",WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

   Print("start server ok");
  }
//------------------------------------------------------------------    Accept
void AcceptClients() // Accept a client socket
  {
   if(server==INVALID_SOCKET) return;

// añadimos a todos los clientes en espera
   SOCKET client=INVALID_SOCKET;
   do
     {
      ref_sockaddr ch; int len=sizeof(ref_sockaddr);
      client=accept(server,ch.ref,len);
      if(client==INVALID_SOCKET)
        {
         int err=WSAGetLastError();
         if(err==WSAEWOULDBLOCK) Comment("\nWAITING CLIENT ("+string(TimeCurrent())+")");
         else { Print("Accept failed with error: ",WSAErrorDescript(err)); CloseClean(); }
         return;
        }

      // activamos el modo sin bloqueo
      int non_block=1;
      int res=ioctlsocket(client, (int)FIONBIO, non_block);
      if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); continue; }

      // añadimos el socket del cliente a la matriz
      int n=ArraySize(conns); ArrayResize(conns,n+1);
      conns[n]=client;
      bChangeTrades=true; // ponemos la bandera sobre la necesidad de enviar información de la posición

                          // mostramos la información sobre el cliente
      char ipstr[23]={0};
      sockaddr_in aclient=(sockaddr_in)ch; //transformamos en una estructura, para recibir información adicional sobre la conexión
      inet_ntop(aclient.sin_family,aclient.sin_addr,ipstr,sizeof(ipstr)); // conseguimos la dirección
      printf("Accept new client %s : %d",CharArrayToString(ipstr),ntohs(aclient.sin_port));
     }
   while(client!=INVALID_SOCKET);
  }
//------------------------------------------------------------------    SendClient
void Send()
  {
   int len=ArraySize(data);
   for(int i=ArraySize(conns)-1; i>=0; --i) // enviamos la información a los clientes
     {
      if(conns[i]==INVALID_SOCKET) continue; // omitir los cerrados
      int res=send(conns[i],data,len,0); // enviamos
      if(res==SOCKET_ERROR) { Print("-Send failed error: "+WSAErrorDescript(WSAGetLastError())+". close socket"); Close(conns[i]); }
     }
  }
//------------------------------------------------------------------    CloseClean
void CloseClean() // cerramos y limpiamos la operación
  {
   printf("Shutdown server and %d connections",ArraySize(conns));
   if(server!=INVALID_SOCKET) { closesocket(server); server=INVALID_SOCKET; } // cerramos el servidor
   for(int i=ArraySize(conns)-1; i>=0; --i) Close(conns[i]); // cerramos a los clientes
   ArrayResize(conns,0);
   WSACleanup();
  }
//------------------------------------------------------------------    Close
void Close(SOCKET &asock) // cerramos un socket
  {
   if(asock==INVALID_SOCKET) return;
   if(shutdown(asock,SD_BOTH)==SOCKET_ERROR) Print("-Shutdown failed error: "+WSAErrorDescript(WSAGetLastError()));
   closesocket(asock);
   asock=INVALID_SOCKET;
  }
//------------------------------------------------------------------    GetSymbolLot
double GetSymbolLot(string smb)
  {
   double slot=0;
   int n=PositionsTotal();
   for(int i=0; i<n;++i)
     {
      PositionSelectByTicket(PositionGetTicket(i));
      if(PositionGetString(POSITION_SYMBOL)!=smb) continue; // filtramos la posición del símbolo actual donde está en marcha el servidor
      double lot=PositionGetDouble(POSITION_VOLUME); // tomamos el volumen
      if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) lot=-lot; // consideramos la dirección
      slot+=lot; // añadimos a la suma
     }
   return slot;
  }


Una vez por segundo se comprueban los bloques clave del servidor: la conexión del cliente y su adición a la matriz total -> Envío de nuevos datos. Asimismo, se comprueba la actividad del propio socket del servidor y, en caso de corte, se crea de nuevo el socket del servidor.

Enviamos a los clientes el nombre del símbolo en el que funciona el experto y el volumen de su posición.

Cada operación comercial enviará el símbolo y el volumen en forma de mensajes:

<<GBPUSD|0.25>>
<<GBPUSD|0.00>>

El envío tiene lugar con cada evento comercial, y también al conectarse un nuevo cliente.

El código del cliente se implementa en forma de experto, puesto que hay que mantener la conexión de forma constante. El cliente recibe del servidor una nueva porción de datos y la añade a la existente. A continuación, busca el símbolo del comienzo << y del final del mensaje >>, lo parsea y ajusta su volumen de acuerdo con el del servidor para el símbolo indicado.

//+------------------------------------------------------------------+
//|                                                     SignalClient |
//|                       programming & development - Alexey Sergeev |
//+------------------------------------------------------------------+
#property copyright "© 2006-2016 Alexey Sergeev"
#property link      "profy.mql@gmail.com"
#property version   "1.00"

#include "SocketLib.mqh"
#include <Trade\Trade.mqh>

input string Host="127.0.0.1";
input ushort Port=8081;

SOCKET client=INVALID_SOCKET; // socket de cliente
string msg=""; // cola de mensajes recibidos
//------------------------------------------------------------------    OnInit
int OnInit()
  {
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {
      Alert("Client work only with Netting accounts"); return INIT_FAILED;
     }

   EventSetTimer(1); return INIT_SUCCEEDED;
  }
//------------------------------------------------------------------    OnInit
void OnDeinit(const int reason) { EventKillTimer(); CloseClean(); }
//------------------------------------------------------------------    OnInit
void OnTimer()
  {
   if(client==INVALID_SOCKET) StartClient(Host,Port);
   else
     {
      uchar data[];
      if(Receive(data)>0) // recibimos los datos
        {
         msg+=CharArrayToString(data); // si se ha recibido algo, lo añadimos a la línea total
         printf("received msg from server: %s",msg);
        }
      CheckMessage();
     }
  }
//------------------------------------------------------------------    CloseClean
void StartClient(string addr,ushort port)
  {
// inicializamos la biblioteca
   int res=0;
   char wsaData[]; ArrayResize(wsaData, sizeof(WSAData));
   res=WSAStartup(MAKEWORD(2,2), wsaData);
   if (res!=0) { Print("-WSAStartup failed error: "+string(res)); return; }

// creamos el socket
   client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
   if(client==INVALID_SOCKET) { Print("-Create failed error: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

// nos conectamos al servidor
   char ch[]; StringToCharArray(addr,ch);
   sockaddr_in addrin;
   addrin.sin_family=AF_INET;
   addrin.sin_addr=inet_addr(ch);
   addrin.sin_port=htons(port);

   ref_sockaddr ref=(ref_sockaddr)addrin;
   res=connect(client,ref.ref,sizeof(addrin));
   if(res==SOCKET_ERROR)
     {
      int err=WSAGetLastError();
      if(err!=WSAEISCONN) { Print("-Connect failed error: "+WSAErrorDescript(err)); CloseClean(); return; }
     }

// activamos el modo sin bloqueo
   int non_block=1;
   res=ioctlsocket(client,(int)FIONBIO,non_block);
   if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); CloseClean(); return; }

   Print("connect OK");
  }
//------------------------------------------------------------------    Receive
int Receive(uchar &rdata[]) // Receive until the peer closes the connection
  {
   if(client==INVALID_SOCKET) return 0; // si el socket no se ha abierto todavía

   char rbuf[512]; int rlen=512; int r=0,res=0;
   do
     {
      res=recv(client,rbuf,rlen,0);
      if(res<0)
        {
         int err=WSAGetLastError();
         if(err!=WSAEWOULDBLOCK) { Print("-Receive failed error: "+string(err)+" "+WSAErrorDescript(err)); CloseClean(); return -1; }
         break;
        }
      if(res==0 && r==0) { Print("-Receive. connection closed"); CloseClean(); return -1; }
      r+=res; ArrayCopy(rdata,rbuf,ArraySize(rdata),0,res);
     }
   while(res>0 && res>=rlen);
   return r;
  }
//------------------------------------------------------------------    CloseClean
void CloseClean() // close socket
  {
   if(client!=INVALID_SOCKET)
     {
      if(shutdown(client,SD_BOTH)==SOCKET_ERROR) Print("-Shutdown failed error: "+WSAErrorDescript(WSAGetLastError()));
      closesocket(client); client=INVALID_SOCKET;
     }

   WSACleanup();
   Print("close socket");
  }
//------------------------------------------------------------------    CheckMessage
void CheckMessage()
  {
   string pos;
   while(FindNextPos(pos)) { printf("server position: %s",pos); };  // tomamos del servidor el cambio más reciente
   if(StringLen(pos)<=0) return;
// recibimos los datos del mensaje
   string res[]; if(StringSplit(pos,'|',res)!=2) { printf("-wrong pos info: %s",pos); return; }
   string smb=res[0]; double lot=NormalizeDouble(StringToDouble(res[1]),2);

// sincronizamos el volumen
   if(!SyncSymbolLot(smb,lot)) msg="<<"+pos+">>"+msg; // si hay un error, entonces retornamos un mensaje al inicio del "hilo"
  }
//------------------------------------------------------------------    SyncSymbolLot
bool SyncSymbolLot(string smb,double nlot)
  {
// sincronizamos el volumen del servidor y el cliente
   CTrade trade;
   double clot=GetSymbolLot(smb); // obtenemos el lote actual del símbolo
   if(clot==nlot) { Print("nothing change"); return true; } // si los volúmenes son iguales, entonces no hacemos nada

                                                            // primero comprobamos el caso particular de ausencia de posiciones en el servidor
   if(nlot==0 && clot!=0) { Print("full close position"); return trade.PositionClose(smb); }

// si el servidor tiene una posición, entonces lo cambiamos en el cliente
   double dif=NormalizeDouble(nlot-clot,2);
// compramos o vendemos dependiendo de la diferencia
   if(dif>0) { Print("add Buy position"); return trade.Buy(dif,smb); }
   else { Print("add Sell position"); return trade.Sell(-dif,smb); }
  }
//------------------------------------------------------------------    FindNextPos
bool FindNextPos(string &pos)
  {
   int b=StringFind(msg, "<<"); if(b<0) return false; // no hay comienzo del mensaje
   int e=StringFind(msg, ">>"); if(e<0) return false; // no hay final del mensaje

   pos=StringSubstr(msg,b+2,e-b-2); // tomamos el bloque de información
   msg=StringSubstr(msg,e+2); // lo eliminamos del mensaje
   return true;
  }
//------------------------------------------------------------------    GetSymbolLot
double GetSymbolLot(string smb)
  {
   double slot=0;
   int n=PositionsTotal();
   for(int i=0; i<n;++i)
     {
      PositionSelectByTicket(PositionGetTicket(i));
      if(PositionGetString(POSITION_SYMBOL)!=smb) continue; // filtramos la posición del símbolo actual donde está en marcha el servidor
      double lot=PositionGetDouble(POSITION_VOLUME); // tomamos el volumen
      if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) lot=-lot; // consideramos la dirección
      slot+=lot; // añadimos a la suma
     }
   return NormalizeDouble(slot,2);
  }


Y, por fin, una demostración del funcionamiento del servidor y el cliente en pareja:


Ejemplo 3. Colector de ticks

Este ejemplo demuestra los sockets UDP. En él, el servidor esperará del cliente los datos para un símbolo.

El código del servidor es muy sencillo, puesto que ya no hay necesidad de guardar información sobre los clientes y esperar su conexión. Aceleraremos un poco la comprobación de datos en el socket, usando un temporizador de milisegundos:

input string Host="0.0.0.0";
input ushort Port=8082;

SOCKET server=INVALID_SOCKET;

//------------------------------------------------------------------    OnInit
int OnInit() { EventSetMillisecondTimer(300); return 0; }
//------------------------------------------------------------------    OnDeinit
void OnDeinit(const int reason) { EventKillTimer(); CloseClean(); }
//------------------------------------------------------------------    OnTimer
void OnTimer()
  {
   if(server!=INVALID_SOCKET)
     {
      char buf[1024]={0};
      ref_sockaddr ref={0}; int len=ArraySize(ref.ref);
      int res=recvfrom(server,buf,1024,0,ref.ref,len);
      if (res>=0) // recibimos y mostramos los datos
         Print("receive tick from client: ", CharArrayToString(buf));
        else
        {
         int err=WSAGetLastError();
         if(err!=WSAEWOULDBLOCK) { Print("-receive failed error: "+WSAErrorDescript(err)+". Cleanup socket"); CloseClean(); return; }
        }

     }
   else // de lo contratio, ponemos el servidor en marcha
     {
      // inicializamos la biblioteca
      char wsaData[]; ArrayResize(wsaData,sizeof(WSAData));
      int res=WSAStartup(MAKEWORD(2,2), wsaData);
      if(res!=0) { Print("-WSAStartup failed error: "+string(res)); return; }

      // creamos el socket
      server=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
      if(server==INVALID_SOCKET) { Print("-Create failed error: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

      // enlazamos con la dirección y el puerto
      Print("try bind..."+Host+":"+string(Port));

      char ch[]; StringToCharArray(Host,ch);
      sockaddr_in addrin;
      addrin.sin_family=AF_INET;
      addrin.sin_addr=inet_addr(ch);
      addrin.sin_port=htons(Port);
      ref_sockaddr ref=(ref_sockaddr)addrin;
      if(bind(server,ref.ref,sizeof(addrin))==SOCKET_ERROR)
        {
         int err=WSAGetLastError();
         if(err!=WSAEISCONN) { Print("-Connect failed error: "+WSAErrorDescript(err)+". Cleanup socket"); CloseClean(); return; }
        }

      // activamos el modo sin bloqueo
      int non_block=1;
      res=ioctlsocket(server,(int)FIONBIO,non_block);
      if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); CloseClean(); return; }

      Print("start server ok");
     }
  }
//------------------------------------------------------------------    CloseClean
void CloseClean() // cerramos y limpiamos la operación
  {
   printf("Shutdown server");
   if(server!=INVALID_SOCKET) { closesocket(server); server=INVALID_SOCKET; } // cerramos el servidor
   WSACleanup();
  }


El código del cliente también es sencillo. Todo el trabajo tiene lugar en el evento de llegada del tick:

input string Host="127.0.0.1";
input ushort Port=8082;

SOCKET client=INVALID_SOCKET; // socket de cliente
ref_sockaddr srvaddr={0}; // estructura para la conexión al servidor
//------------------------------------------------------------------    OnInit
int OnInit()
  {
// rellenamos la estructura para el servidor
   char ch[]; StringToCharArray(Host,ch);
   sockaddr_in addrin;
   addrin.sin_family=AF_INET;
   addrin.sin_addr=inet_addr(ch);
   addrin.sin_port=htons(Port);
   srvaddr=(ref_sockaddr)addrin;

   OnTick(); // creamos el socket de inmediato

   return INIT_SUCCEEDED;
  }
//------------------------------------------------------------------    OnDeinit
void OnDeinit(const int reason) { CloseClean(); }
//------------------------------------------------------------------    OnTick
void OnTick()
  {
   if(client!=INVALID_SOCKET) // si el socket ya se ha creado, entonces enviamos
     {
      uchar data[]; StringToCharArray(Symbol()+" "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_BID),Digits()),data);
      if(sendto(client,data,ArraySize(data),0,srvaddr.ref,ArraySize(srvaddr.ref))==SOCKET_ERROR)
        {
         int err=WSAGetLastError();
         if(err!=WSAEWOULDBLOCK) { Print("-Send failed error: "+WSAErrorDescript(err)); CloseClean(); }
        }
      else
         Print("send "+Symbol()+" tick to server");
     }
   else // creamos el socket de cliente
     {
      int res=0;
      char wsaData[]; ArrayResize(wsaData,sizeof(WSAData));
      res=WSAStartup(MAKEWORD(2,2),wsaData);
      if(res!=0) { Print("-WSAStartup failed error: "+string(res)); return; }

      // creamos el socket
      client=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
      if(client==INVALID_SOCKET) { Print("-Create failed error: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); return; }

      // activamos el modo sin bloqueo
      int non_block=1;
      res=ioctlsocket(client,(int)FIONBIO,non_block);
      if(res!=NO_ERROR) { Print("ioctlsocket failed error: "+string(res)); CloseClean(); return; }

      Print("create socket OK");
     }
  }
//------------------------------------------------------------------    CloseClean
void CloseClean() // close socket
  {
   if(client!=INVALID_SOCKET) { closesocket(client); client=INVALID_SOCKET; }
   WSACleanup();
   Print("close socket");
  }

Y aquí demostramos su funcionamiento final:



3. Caminos posteriores para reforzar el servidor

Resulta obvio que estos ejemplos, que envían información a cualquier cliente, no son los óptimos. Por ejemplo, usted seguro que querrá limitar el acceso a su información.  Significa que, como mínimo, deberá tener los siguientes requisitos:

Asimismo habrá notado que todo el funcionamiento del servidor se encuentra solo en un hilo (en el temporizador de un experto). Esto resulta crítico cuando se tiene una gran cantidad de conexiones o volumen de información. Por eso, para optimizar el servidor hay que añadir como mínimo un pool de expertos (cada uno con su temporizador), en los que tendrá lugar la interacción de las conexiones del cliente. Esto, en cierta medida, convierte al servidor en multihilo.

Será su decisión si quiere hacer todo esto en el marco de MQL. Para ello hay otros métodos que le pueden ser más cómodos. Pero el hecho de que MQL le dé ventajas a la hora de acceder directamente al comercio de la cuenta y a las cotizaciones es indiscutible, así como también lo es el carácter abierto del código MQL, que no usa DLL externos.


Conclusión

¿De qué otras formas se pueden usar los sockets en MetaTrader? Antes de escribir el artículo, tenía varias ideas para mostrar como ejemplos a analizar:

Podemos pensar una gran variedad de posibilidades. Si usted tiene también ideas que aplicar, compártalas en los comentarios al artículo. Es posible que resulten interesantes y podamos implementarlas juntos.

¡Le deseo suerte y beneficios!