Une solution sans DLL pour communiquer entre les terminaux MetaTrader 5 à l'aide de Canaux Nommés

investeo | 17 novembre, 2021

Introduction

Je me suis interrogé pendant un certain temps sur les moyens de communication possibles entre les terminaux MetaTrader 5. Mon objectif était d'utiliser l'indicateur de tick et d'afficher les ticks de différents fournisseurs de cotations dans l'un des terminaux.

La solution naturelle consistait à utiliser des fichiers séparés sur un disque dur. Un terminal écrirait des données dans le fichier et l'autre les lirait. Cette méthode bien que pertinente pour l'envoi de messages uniques ne semble pas être la plus efficace pour les offres en streaming.

Ensuite, je suis tombé sur un bon article d'Alexander sur la façon d'exporter des devis vers des applications .NET à l'aide des services WCF et quand j'étais sur le point de terminer, un autre article de Sergeev est apparu.

Les deux articles étaient proches de ce dont j'avais besoin mais j'ai cherché une solution sans DLL qui pourrait être utilisée par différents terminaux, l'un servant de serveur et l'autre de client. En cherchant sur le Web, j'ai trouvé une note suggérant que l'on pouvait utiliser des Canaux Nommés pour la communication et j'ai lu attentivement la spécification cahier des charges MSDN pour la communication inter-processus utilisant les canaux.

J'ai découvert que les Canaux Nommés prennent en charge la communication sur le même ordinateur ou sur différents ordinateurs via intranet. J'ai décidé d'opter pour cette approche.

Cet article présente la communication Canaux Nommés et décrit un processus de conception de la classe CNamedPipes. Il comprend également le test du flux d'indicateurs de tick entre les terminaux MetaTrader 5 et le débit global du système.

1. Communication inter-processus à l'aide de Canaux Nommés

Lorsque nous pensons à un canal typique, nous imaginons une sorte de cylindre utilisé pour véhiculer des médias. C'est également un terme utilisé pour l'un des moyens de communication inter-processus sur un système d'exploitation. Vous pourriez simplement imaginer un canal qui relie deux processus, dans notre cas des terminaux MetaTrader 5 qui échangent des données. 

Les canaux peuvent être anonymes ou nommés. Il existe deux différences principales entre eux : la première est que les canaux anonymes ne peuvent pas être utilisés sur un réseau et la seconde que deux processus doivent être liés. C'est-à-dire qu'un processus doit être un parent et l'autre un processus enfant. Les Canaux Nommés n'ont pas cette limitation.

Afin de communiquer à l'aide de canaux, un processus serveur doit configurer un canal avec un nom connu. Le nom du canal est une chaîne et doit être sous la forme \\servername\pipe\pipename. Si des canaux sont utilisées sur le même ordinateur, servername peut être omis et un point peut être placé à la place : \\.\pipe\pipename.

Le client qui essaie de se connecter à un canal doit connaître son nom. J'utilise une convention de nom de \\.\pipe\mt[account_number] afin de distinguer les terminaux, mais la convention de nommage peut être modifiée arbitrairement.

2. Implémentation de la classe CNamedPipes

Je commencerai par une brève description du mécanisme de bas niveau de création et de connexion à un canal nommé. Sur les systèmes d'exploitation Windows, toutes les fonctions qui gèrent les canaux sont disponibles via la bibliothèque kernel32.dll. La fonction instanciant un canal nommé côté serveur estCreateNamedPipe()..

Une fois le canal créé, le serveur appelle ConnectNamedPipe() fonction pour attendre qu'un client se connecte. Si la connexion réussit, ConnectNamedPipe() renvoie un entier différent de zéro. Il est cependant possible que le client se soit connecté avec succès après avoir appelé CreateNamedPipe() et avant ConnectNamedPipe() a été appelé. Dans ce cas ConnectNamedPipe() renvoie zéro et GetLastError() renvoie l'erreur 535 (0X217) : ERROR_PIPE_CONNECTED.

L'écriture et la lecture à partir d'un canal sont réalisées avec les mêmes fonctions que pour l'accès aux fichiers :

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
);

Ayant appris les canaux nommés, j'ai conçu la classe CNamedPipes afin de masquer les instructions de bas niveau sous-jacentes.

Il suffit maintenant de mettre le fichier CNamedPipes.mqh dans le dossier approprié et (/include) du terminal et de l'inclure dans le code source et de déclarer un objet CNamedPipe.

La classe que j'ai conçue expose quelques méthodes de base pour gérer les Canaux Nommés :

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

La classe peut être étendue en fonction d'exigences supplémentaires.

La méthode Create() essaie de créer un canal avec un nom donné. Pour simplifier la connexion entre les terminaux, le paramètre d'entrée 'compte' est le numéro de compte d'un client qui utilisera un canal.

Si le nom de compte n'est pas entré, la méthode essaie d'ouvrir un canal avec le numéro de compte d'un terminal actuel. La fonction Create() renvoie true si le canal a été créé avec succès.

//+------------------------------------------------------------------+
/// 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;

  }

La méthode Connect() attend qu'un client se connecte à un canal. Il renvoie true si le client s'est connecté avec succès à un canal.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode Disconnect() déconnecte le serveur d'un canal.

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

La méthode Open() doit être utilisée par un client, elle essaie d'ouvrir un canal précédemment créé. Il renvoie vrai si l'ouverture du canal a abouti.  Il renvoie false si, pour une raison quelconque, il n'a pas pu se connecter au canal créé dans un délai de 5 secondes ou si l'ouverture du canal a échoué.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode Close() ferme la poignée du canal.

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

Les six méthodes suivantes sont utilisées pour lire et écrire dans les canaux. Les deux premières paires de poignées gèrent les chaînes aux formats Unicode et ANSI, les deux peuvent être utilisées pour envoyer des commandes ou des messages entre les terminaux.

La variable de chaîne dans MQL5 est stockée en tant qu'objet qui contient Unicode, donc la manière naturelle était de fournir des méthodes Unicode, mais puisque MQL5 fournit des méthodes UnicodeToANSI, j'ai également implémenté la communication de chaîne ANSI. Les deux dernières méthodes gèrent l'envoi et la réception d'objets MqlTick via un canal nommé. 

La méthode WriteUnicode() rédige le message composé de caractères Unicode. Étant donné que chaque caractère se compose de deux octets, il est envoyé sous forme de tableau de ushort à un canal.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode ReadUnicode() reçoit un tableau d'ushorts et renvoie un objet chaîne.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode WriteANSI() écrit le tableau uchar ANSI dans un canal.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode ReadANSI() lit le tableau uchar à partir d'un canal et renvoie un objet chaîne.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode WriteTick() écrit un seul objet MqlTick dans un canal.

//+------------------------------------------------------------------+
/// 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;
  }

La méthode ReadTick() lit un seul objet MqlTick à partir d'un canal. Si un canal est vide, il renvoie 0, sinon il doit renvoyer un certain nombre d'octets de l'objet 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;
  }
//+------------------------------------------------------------------+

Puisque les méthodes de base de gestion des canaux nommés sont connues, nous pouvons commencer par deux programmes MQL : un script simple pour recevoir des offres et un indicateur pour envoyer des offres.

3. Script de serveur pour la réception de offres

L'exemple de serveur lance le canal nommé et attend qu'un client se connecte. Une fois le client déconnecté, il affiche le nombre de ticks reçus par ce client au total et attend qu'un nouveau client se connecte. Si le client est déconnecté et que le serveur trouve une variable globale 'gvar0', il se ferme. Si la variable 'gvar0' n'existe pas, vous pouvez arrêter manuellement le serveur en cliquant sur le bouton droit sur un graphique et en choisissant l'option Liste d'experts.

//+------------------------------------------------------------------+
//|                                              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. Indicateur simple pour l'envoi de offres

L'indicateur d'envoi de devis ouvre un canal dans la méthode OnInit() et envoie un seul MqlTick à chaque fois que la méthode OnCalculate() est déclenchée :
//+------------------------------------------------------------------+
//|                                        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. Exécution d'indicateurs de tick de plusieurs fournisseurs dans un seul terminal client

La situation s'est compliquée davantage car je voulais afficher les offres entrantes dans des indicateurs de tick séparés. J'y suis parvenu en implémentant un serveur de canaux qui diffuse les ticks entrants à l'indicateur de tick en déclenchant la méthode EventChartCustom ().

Les soumissions et les demandes de devis sont envoyées sous la forme d'une seule chaîne divisée par un point-virgule, par exemple '1.20223;120225'. L'indicateur approprié gère un événement personnalisé dans OnChartEvent() et affiche un graphique en 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(); 
  }

Afin d'afficher les ticks, j'ai choisi l'indicateur de tick placé dans MQLmagazine, mais au lieu de la méthode OnCalculate() j'ai implémenté le traitement dans OnChartEvent() et ajout d'instructions conditionnelles. Une offre est acceptée pour le traitement uniquement si le paramètre dparam est égal au numéro de canal et que l'ID d'événement est égal à 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 
        }
  }

Sur la capture d'écran ci-dessous, il y a trois indicateurs de tick.

Deux d'entre eux affichent les ticks reçus via des canaux et un troisième indicateur qui n'utilise pas de canaux a été exécuté pour vérifier si aucun tick n'a été perdu.  

Indicateur de coche avec des données de différents terminaux

Fig. 1 Offres reçues via un canal nommé

Veuillez trouver ci-joint une vidéographie avec des commentaires sur la façon dont j'exécute les indicateurs :

Fig. 2 vidéographie décrivant la configuration des indicateurs

6. Tester le Débit du Système

Étant donné que les canaux utilisent la mémoire partagée, la communication est très rapide. J'ai effectué des tests d'envoi de 100 000 et 1 000 000 de ticks d'affilée entre deux terminaux MetaTrader 5. Le script d'envoi utilise la fonction WriteTick() et mesure la durée à l'aide de 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();

Le serveur lit les offres entrantes. La durée est mesurée à partir de la première offre entrante jusqu'à la déconnexion du client :

//+------------------------------------------------------------------+
//|                                          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]");

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

Les résultats pour 10 séries d'échantillons étaient les suivants :

Courir
offres
Temps d'envoi [ms]
Temps de réception [ms]
1
 100000
 624
624
2  100000  702  702
3  100000  687  687
4  100000  592  608
5  100000  624  624
6  1000000  5616  5616
7  1000000  5788  5788
8  1000000  5928  5913
9
 1000000  5772  5756
10
 1000000  5710  5710

Tableau 1 Mesures de vitesse du débit

La vitesse moyenne d'envoi de 1 000 000 d’offres était d'environ 170 000 ticks/seconde sur un ordinateur portable exécutant Windows Vista avec un processeur T4200 à 2,0 GHz et 3 Go de RAM.

Conclusion

J'ai présenté une méthode de communication entre les terminaux MetaTrader 5 à l'aide de Canaux Nommés La méthode s'est avérée suffisante pour envoyer des offres en temps réel entre les terminaux.

La classe CNamedPipes peut être encore étendue en fonction d'exigences supplémentaires, par exemple pour rendre possible la couverture sur deux comptes indépendants. Veuillez trouver ci-joint le code source de la classe CNamedPipe avec la documentation au format chm et tout autre code source que j'ai implémenté pour écrire l'article.