Solución sin DLL para la comunicación entre terminales MetaTrader 5 mediante canalizaciones con nombre

investeo | 11 marzo, 2014


Introducción

Me preguntaba a veces sobre los posibles modos de comunicación entre los terminales MetaTrader 5. Mi objetivo era utilizar el tick del indicador y los ticks mostrados por varios proveedores de cotizaciones en uno de los terminales.

La solución natural era utilizar archivos separados en un disco duro. Un terminal para escribir los datos en el archivo y el otro para leerlo. Este método, a pesar de ser apropiado para enviar un único mensaje, no parece ser el más efectivo con los flujos de cotizaciones.

Después encontré un buen artículo de Alexander sobre cómo exportar cotizaciones a aplicaciones .NET mediante los servicios WCF y cuando estaba a punto de acabar, apareció otro artículo de Sergeev.

Ambos artículos se aproximaban a lo que necesitaba, pero buscaba una solución sin DLL y que pueda ser utilizada por distintos terminales, uno haciendo de Servidor y el otro de Cliente. Mientras buscaba en la Web, encontré una nota sugiriendo que se pueden utilizar las canalizaciones con nombre para la comunicación y he leído a fondo las especificaciones de MSDN para la comunicación entre procesos mediante canalizaciones.

Descubrí que las Canalizaciones con nombre respaldan las comunicaciones a través del mismo ordenador o varios ordenadores a través de la intranet, por lo que decidí utilizar este método.

En este artículo se presenta la comunicación por Canalizaciones con nombre y se describe el proceso de diseño de la clase CNamedPipes. También incluye el flujo de ticks de prueba del indicador entre los terminales MetaTrader 5 y el rendimiento general del sistema.

1. La comunicación entre procesos mediante canalizaciones con nombre

Cuando pensamos en una canalización típica, pensamos un en tubo utilizado para transportar los medios. También es un término empleado para referirse a la comunicación entre procesos en un sistema operativo. Simplemente tiene que imaginar una tubería que conecta dos procesos, en nuestro caso, los terminales de MetaTrader 5 intercambiando datos. 

Las canalizaciones pueden ser anónimas o con nombre. Hay dos diferencias principales entre las dos: la primera es que las canalizaciones anónimas no se pueden utilizar en una red y la segunda es que dos procesos tienen que estar relacionados. Es decir que un proceso tiene que ser el padre y el otro proceso el hijo. Las canalizaciones con nombre no tienen esta limitación.

Un servidor tiene que configurar una canalización con un nombre conocido para poder comunicar mediante canalizaciones. El nombre de una canalización es una cadena y debe incluir \\servername\pipe\pipename. En el caso de utilizar las canalizaciones en el mismo ordenador, se puede sustituir el nombre del ordenador por un punto:  \\.\pipe\pipename.

El cliente que quiere conectarse a la canalización tiene que conocer su nombre. Estoy utilizando la convención de nomenclatura \\.\pipe\mt[account_number] para poder distinguir los terminales, pero se puede cambiar sin ningún criterio.

2. Implementación de la clase CNamedPipes

Voy a comenzar con una breve descripción del mecanismo de bajo nivel para la creación y la conexión a una canalización con nombre. En el sistema operativo Windows, todas las funciones que manejan las canalizaciones están disponibles en la librería kernel32.dll. La función para crear instancias de una canalización con nombre en el lado del servidor es CreateNamedPipe().

Después de crear la canalización, el servidor llama a la función ConnectNamedPipe() y espera la conexión de un cliente. Si la conexión se establece satisfactoriamente, la función ConnectNamedPipe() devuelve un entero distinto de cero. Pero puede ser que el cliente se conecte correctamente después de llamar CreateNamedPipe() y antes de la llamada a ConnectNamedPipe(). En este caso ConnectNamedPipe() devuelve cero, y GetLastError() devuelve el error 535 (0X217) : ERROR_PIPE_CONNECTED.

La escritura y la lectura en una canalización se hacen con las mismas funciones de acceso a los archivos:

BOOL WINAPI ReadFile(
  __in         HANDLE hFile,
  __out        LPVOID lpBuffer,
  __in         DWORD nNumberOfBytesToRead,
  __out_opt    LPDWORD lpNumberOfBytesRead,
  __inout_opt  LPOVERLAPPED lpOverlapped
);

BOOL WINAPI WriteFile(
  __in         HANDLE hFile,
  __in         LPCVOID lpBuffer,
  __in         DWORD nNumberOfBytesToWrite,
  __out_opt    LPDWORD lpNumberOfBytesWritten,
  __inout_opt  LPOVERLAPPED lpOverlapped
);

Después de haber asimilado las canalizaciones con nombre, he diseñado la clase CNamedPipes para ocultar las estructuras subyacentes de bajo nivel.

Basta con poner el archivo CNamedPipes.mqh en la carpeta apropiada (/include) del terminal e incluirlo en el código fuente y declarar un objeto CNamedPipe.

La clase que he diseñado ofrece algunos métodos básicos para manejar las canalizaciones con nombre:

Create(), Connect(), Disconnect(), Open(), Close(), WriteUnicode(), ReadUnicode(), WriteANSI(), ReadANSI(), WriteTick(), ReadTick()

Se puede ampliar la clase aún más según los requisitos adicionales.

El método Create() trata de crear la canalización con un nombre indicado. Para simplificar la conexión entre terminales, el parámetro de entrada 'account' es el número de cuenta del cliente que va a utilizar la canalización.

Si no se introduce un nombre de cuenta, el método intenta abrir una canalización con el número de cuenta actual del terminal. Si se crea la canalización correctamente, la función Create() devuelve el valor true.

//+------------------------------------------------------------------+
/// Create() : try to create a new instance of Named Pipe            |
/// \param account - source terminal account number                  |
/// \return true - if created, false otherwise                       |
//+------------------------------------------------------------------+
bool CNamedPipe::Create(int account=0)
  {
   if(account==0)
      pipeNumber=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
   else
      pipeNumber=IntegerToString(account);

   string fullPipeName=pipeNamePrefix+pipeNumber;

   hPipe=CreateNamedPipeW(fullPipeName,
                          (int)GENERIC_READ|GENERIC_WRITE|(ENUM_PIPE_ACCESS)PIPE_ACCESS_DUPLEX,
                          (ENUM_PIPE_MODE)PIPE_TYPE_RW_BYTE,PIPE_UNLIMITED_INSTANCES,
                          BufferSize*sizeof(ushort),BufferSize*sizeof(ushort),0,NULL);

   if(hPipe==INVALID_HANDLE_VALUE) return false;
   else
      return true;

  }

El método Connect() espera a que un cliente se conecte a la canalización. Si el cliente se conecta correctamente a la canalización, entonces devuelve el valor true.

//+------------------------------------------------------------------+
/// Connect() : wait for a client to connect to a pipe               |
/// \return true - if connected, false otherwise.                    |
//+------------------------------------------------------------------+
bool CNamedPipe::Connect(void)
  {
   if(ConnectNamedPipe(hPipe,NULL)==false)
      return(kernel32::GetLastError()==ERROR_PIPE_CONNECTED);
   else return true;
  }

El método Disconnect() desconecta el servidor de la canalización.

//+------------------------------------------------------------------+
/// Disconnect(): disconnect from a pipe                             |
/// \return true - if disconnected, false otherwise                  |
//+------------------------------------------------------------------+
bool CNamedPipe::Disconnect(void)
  {
   return DisconnectNamedPipe(hPipe);
  }


El método Open() debe ser utilizado por un cliente, trata de abrir la canalización creada previamente. Si se abre la canalización correctamente, devuelve el valor true.  Si por alguna razón no puede conectarse a la canalización creada durante 5 segundos o si hay un fallo en la conexión, devuelve false.

//+------------------------------------------------------------------+
/// Open() : try to open previously created pipe                     |
/// \param account - source terminal account number                  |
/// \return true - if successfull, false otherwise.                  |
//+------------------------------------------------------------------+
bool CNamedPipe::Open(int account=0)
  {
   if(account==0)
      pipeName=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
   else
      pipeName=IntegerToString(account);

   string fullPipeName=pipeNamePrefix+pipeName;

   if(hPipe==INVALID_HANDLE_VALUE)
     {
      if(WaitNamedPipeW(fullPipeName,5000)==0)
        {
         Print("Pipe "+fullPipeName+" not available...");
         return false;
        }

      hPipe=CreateFileW(fullPipeName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
      if(hPipe==INVALID_HANDLE_VALUE)
        {
         Print("Pipe open failed");
         return false;
        }

     }
   return true;
  }

El método Close() cierra el identificador de la canalización.

//+------------------------------------------------------------------+
/// Close() : close pipe handle                                      |
/// \return 0 if successfull, non-zero otherwise                     |
//+------------------------------------------------------------------+
int CNamedPipe::Close(void)
  {
   return CloseHandle(hPipe);
  }

Los siguientes seis métodos se utilizan para leer y escribir a través de las canalizaciones. Los dos primeros pares manejan las cadenas en los formatos Unicode y ANSI, y se pueden utilizar ambos para enviar comandos o mensajes entre terminales.

En MQL5, la variable string (cadena) se almacena como un objeto que contiene Unicode, es por ello que la forma natural era proporcionar métodos Unicode, pero puesto que MQL 5 proporciona métodos UnicodeToANSI, he implementado también la cadena de comunicación ANSI. Los dos últimos métodos manejan el envío y la recepción de objetos MqlTick a través de una canalización con nombre. 

El método WriteUnicode() escribe mensajes con caracteres Unicode. Dado que cada carácter está compuesto de dos bytes, se envía como una matriz ushort a la canalización.

//+------------------------------------------------------------------+
/// WriteUnicode() : write Unicode string to a pipe                  |
/// \param message - string to send                                  |
/// \return number of bytes written to a pipe                        |
//+------------------------------------------------------------------+
int CNamedPipe::WriteUnicode(string message)
  {
   int ushortsToWrite, bytesWritten;
   ushort UNICODEarray[];
   ushortsToWrite = StringToShortArray(message, UNICODEarray);
   WriteFile(hPipe,ushortsToWrite,sizeof(int),bytesWritten,0);
   WriteFile(hPipe,UNICODEarray,ushortsToWrite*sizeof(ushort),bytesWritten,0);
   return bytesWritten;
  }

El método ReadUnicode() recibe la matriz ushort y devuelve un objeto string.

//+------------------------------------------------------------------+
/// ReadUnicode(): read unicode string from a pipe                   |
/// \return unicode string (MQL5 string)                             |
//+------------------------------------------------------------------+
string CNamedPipe::ReadUnicode(void)
  {
   string ret;
   ushort UNICODEarray[STR_SIZE*sizeof(uint)];
   int bytesRead, ushortsToRead;
 
   ReadFile(hPipe,ushortsToRead,sizeof(int),bytesRead,0);
   ReadFile(hPipe,UNICODEarray,ushortsToRead*sizeof(ushort),bytesRead,0);
   if(bytesRead!=0)
      ret = ShortArrayToString(UNICODEarray);
   
   return ret;
  }

El método WriteANSI() escribe una matriz uchar en la canalización.

//+------------------------------------------------------------------+
/// WriteANSI() : write ANSI string to a pipe                        |
/// \param message - string to send                                  |
/// \return number of bytes written to a pipe                        |
//+------------------------------------------------------------------+
int CNamedPipe::WriteANSI(string message)
  {
   int bytesToWrite, bytesWritten;
   uchar ANSIarray[];
   bytesToWrite = StringToCharArray(message, ANSIarray);
   WriteFile(hPipe,bytesToWrite,sizeof(int),bytesWritten,0);
   WriteFile(hPipe,ANSIarray,bytesToWrite,bytesWritten,0);
   return bytesWritten;
  }

El método ReadANSI() lee la matriz uchar a partir de la canalización y devuelve un objeto string.

//+------------------------------------------------------------------+
/// ReadANSI(): read ANSI string from a pipe                         |
/// \return unicode string (MQL5 string)                             |
//+------------------------------------------------------------------+
string CNamedPipe::ReadANSI(void)
  {
   string ret;
   uchar ANSIarray[STR_SIZE];
   int bytesRead, bytesToRead;
 
   ReadFile(hPipe,bytesToRead,sizeof(int),bytesRead,0);
   ReadFile(hPipe,ANSIarray,bytesToRead,bytesRead,0);
   if(bytesRead!=0)
      ret = CharArrayToString(ANSIarray);
   
   return ret;
  }

El método WriteTick() escribe un objeto único MqlTick en una canalización.

//+------------------------------------------------------------------+
/// WriteTick() : write MqlTick to a pipe                            |
/// \param MqlTick to send                                           |
/// \return true if tick was written correctly, false otherwise      |
//+------------------------------------------------------------------+
int CNamedPipe::WriteTick(MqlTick &outgoing)
  {
   int bytesWritten;

   WriteFile(hPipe,outgoing,MQLTICK_SIZE,bytesWritten,0);

   return bytesWritten;
  }

El método ReadTick() lee un objeto único MqlTick a partir de una canalización. Si la canalización está vacía, devuelve 0, de lo contrario, devuelve el número de bytes del objeto MqlTick.

//+------------------------------------------------------------------+
/// ReadTick() : read MqlTick from a pipe                            |
/// \return true if tick was read correctly, false otherwise         |
//+------------------------------------------------------------------+
int CNamedPipe::ReadTick(MqlTick &incoming)
  {
   int bytesRead;

   ReadFile(hPipe,incoming,MQLTICK_SIZE,bytesRead,NULL);

   return bytesRead;
  }
//+------------------------------------------------------------------+

Ya que conocemos los métodos básicos para el manejo de las canalizaciones con nombre, podemos empezar con dos programas MQL: un script sencillo para recibir las cotizaciones del indicador y un indicador para enviar las cotizaciones.

3. Script del servidor para la recepción de las cotizaciones

El servidor del ejemplo inicializa la canalización con nombre y espera la conexión de un cliente. Una vez se desconecta el cliente, se muestra el número total de ticks recibidos por este cliente y espera la conexión de un nuevo cliente. Si el cliente se desconecta y el servidor encuentra una variable global 'gvar0', entonces se detiene el servidor. Si la variable 'gvar0' no existe, se puede detener el servidor manualmente, haciendo un clic derecho sobre el gráfico o eligiendo la lista de opciones Expert List.

//+------------------------------------------------------------------+
//|                                              NamedPipeServer.mq5 |
//|                                      Copyright 2010, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#include <CNamedPipes.mqh>

CNamedPipe pipe;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   bool tickReceived;
   int i=0;

   if(pipe.Create()==true)
      while (GlobalVariableCheck("gvar0")==false)
        {
         Print("Waiting for client to connect.");
         if (pipe.Connect()==true)
            Print("Pipe connected");
         while(true)
           {
            do
              {
               tickReceived=pipe.ReadTick();

               if(tickReceived==false)
                 {
                  if(GetError()==ERROR_BROKEN_PIPE)
                    {
                     Print("Client disconnected from pipe "+pipe.Name());
                     pipe.Disconnect();
                     break;
                    }
                 } else i++;
                  Print(IntegerToString(i) + "ticks received.");
              } while(tickReceived==true);
            if (i>0) 
            {
               Print(IntegerToString(i) + "ticks received.");
               i=0;
            };
            if(GlobalVariableCheck("gvar0")==true || (GetError()==ERROR_BROKEN_PIPE)) break;
           }

        }

 pipe.Close(); 
  }

4. Un indicador sencillo para enviar cotizaciones

El indicador para enviar cotizaciones abre una canalización dentro del método OnInit() y envía un único MqlTick cada vez que se activa el método OnCalculate():
//+------------------------------------------------------------------+
//|                                        SendTickPipeIndicator.mq5 |
//|                                      Copyright 2010, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"
#property indicator_chart_window

#include <CNamedPipes.mqh>

CNamedPipe pipe;
int ctx;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
 
   while (!pipe.Open(AccountInfoInteger(ACCOUNT_LOGIN)))
   {
      Print("Pipe not created, retrying in 5 seconds...");
      if (GlobalVariableCheck("gvar1")==true) break;
   }
   
   ctx = 0;
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& open[],
                const double& high[],
                const double& low[],
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
  {
   ctx++;
   MqlTick outgoing;
   SymbolInfoTick(Symbol(), outgoing);
   pipe.WriteTick(outgoing);
   Print(IntegerToString(ctx)+" tick send to server by SendTickPipeClick.");
   return(rates_total);
  }
//+------------------------------------------------------------------+

5. Ejecución de los indicadores de ticks a partir de varios proveedores en un único terminal de cliente

Como quiero mostrar las cotizaciones en indicadores de ticks separados, el panorama se hace más complicado. He conseguido esto mediante la implementación de un servidor de canalizaciones que emite los ticks entrantes al indicador de ticks mediante la activación del método EventChartCustom() .

Las cotizaciones de compra (Bid) y venta (ask) se envían como una cadena única dividida por un punto y coma, por ejemplo '1.20223;120225'. El indicador correspondiente maneja el evento personalizado dentro de OnChartEvent() y muestra un gráfico de ticks. 

//+------------------------------------------------------------------+
//|                                   NamedPipeServerBroadcaster.mq5 |
//|                                      Copyright 2010, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"
#property script_show_inputs
#include <CNamedPipes.mqh>

input int account = 0;

CNamedPipe pipe;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   bool tickReceived;
   int i=0;

   if(pipe.Create(account)==true)
      while(GlobalVariableCheck("gvar0")==false)
        {
         if(pipe.Connect()==true)
            Print("Pipe connected");
            i=0;
         while(true)
           {
            do
              {
               tickReceived=pipe.ReadTick();
               if(tickReceived==false)
                 {
                  if(kernel32::GetLastError()==ERROR_BROKEN_PIPE)
                    {
                     Print("Client disconnected from pipe "+pipe.GetPipeName());
                     pipe.Disconnect();
                     break;
                    }
                  } else  {
                   i++; Print(IntegerToString(i)+" ticks received BY server.");
                  string bidask=DoubleToString(pipe.incoming.bid)+";"+DoubleToString(pipe.incoming.ask);
                  long currChart=ChartFirst(); int chart=0;
                  while(chart<100) 
                    {
                     EventChartCustom(currChart,6666,0,(double)account,bidask);
                     currChart=ChartNext(currChart); 
                     if(currChart==0) break;         // Reached the end of the charts list
                     chart++;
                    }
                     if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break;
              
                 }
              }
            while(tickReceived==true);
            if(i>0)
              {
               Print(IntegerToString(i)+"ticks received.");
               i=0;
              };
            if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break;
            Sleep(100);
           }

        }


  pipe.Close(); 
  }

Para mostrar los ticks he elegido un indicador de ticks colocade en MQLmagazine, pero en lugar del método OnCalculate() he implementado el procesado dentro de OnChartEvent() y añadido instrucciones condicionales. Se acepta el procesamiento de una cotización solo si el parámetro dparam es igual al número de la canalización y el Id del evento es igual a CHARTEVENT_CUSTOM+6666:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
  if (dparam==(double)incomingPipe)
   if(id>CHARTEVENT_CUSTOM)
     {
      if(id==CHARTEVENT_CUSTOM+6666)
        {
        // Process incoming tick
        }
     } else
        {
         // Handle the user event 
        }
  }

En las siguientes capturas de pantalla hay tres indicadores de ticks.

De los cuales dos muestran los ticks recibidos a través de las canalizaciones y un tercero que no utiliza las canalizaciones, y que se estaba ejecutando para comprobar que no se ha perdido ningún tick.  

Indicador de tick con datos procedentes de distintos terminales

Fig. 1 Cotizaciones recibidas a través de una canalización con nombre

Se adjunta un screencast con comentarios sobre cómo he puesto en marcha los indicadores:

Fig. 2 Screencast describiendo la configuración de los indicadores

6. Rendimiento del sistema de pruebas

Puesto que las canalizaciones emplean la memoria compartida, la comunicación es muy rápida. He realizado las pruebas enviando 100.000 y 1.000.000 de ticks seguidos entre dos terminales MetaTrader 5. El script de envío utiliza la función WriteTick() y mide la duración del tiempo mediante GetTickCount():

   Print("Sending...");
   uint start = GetTickCount();
   for (int i=0;i<100000;i++)
      pipe.WriteTick(outgoing);
   uint stop = GetTickCount();
   Print("Sending took" + IntegerToString(stop-start) + " [ms]");
   pipe.Close();

El servidor lee las cotizaciones entrantes. Se mide la duración desde la primera cotización entrante hasta que se desconecte el cliente:

//+------------------------------------------------------------------+
//|                                          SpeedTestPipeServer.mq5 |
//|                                      Copyright 2010, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#property script_show_inputs
#include <CNamedPipes.mqh>

input int account=0;
bool tickReceived;
uint start,stop;

CNamedPipe pipe;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   int i=0;
   if(pipe.Create(account)==true)
      if(pipe.Connect()==true)
         Print("Pipe connected");

   do
     {
      tickReceived=pipe.ReadTick();
      if(i==0) start=GetTickCount();
      if(tickReceived==false)
        {
         if(kernel32::GetLastError()==ERROR_BROKEN_PIPE)
           {
            Print("Client disconnected from pipe "+pipe.GetPipeName());
            pipe.Disconnect();
            break;
           }
        }
      else i++;
     }
   while(tickReceived==true);
   stop=GetTickCount();

   if(i>0)
     {
      Print(IntegerToString(i)+" ticks received.");
      i=0;
     };
   
   pipe.Close();
   Print("Server: receiving took "+IntegerToString(stop-start)+" [ms]");

  }
//+------------------------------------------------------------------+

Los resultados para 10 ejecuciones son los siguientes:

Ejecución
Cotizaciones
Tiempo de envío  [ms]
Tiempo de recepción  [ms]
1
 100.000
 624
624
2  100.000  702  702
3  100.000  687  687
4  100.000  592  608
5  100.000  624  624
6  1.000.000  5616  5616
7  1.000.000  5788  5788
8  1.000.000  5928  5913
9
 1.000.000  5772  5756
10
 1.000.000  5710  5710

Tabla 1 Medidas del rendimiento de velocidad

La velocidad media del envío de 1.000.000 de cotizaciones fue sobre 170.000 ticks/segundo en un portátil bajo Windows Vista con una CPU de 2.0 GHz T4200 y 3 GB de RAM.

Conclusión

He introducido un método de comunicación entre terminales MetaTrader 5 mediante canalizaciones con nombre. El método resultó ser suficiente para el envío de cotizaciones en tiempo real entre terminales.

Se puede ampliar aún más la clase CNamedPipes de acuerdo con otras necesidades, por ejemplo para hacer posible la cobertura de riesgo en dos cuentas independientes. Se adjunta el código fuente de la clase CNamedPipes con la documentación en el formato chm i otros códigos fuente que he implementado para escribir este artículo.