Comment choisir la communication inter-processus sous Windows ── Tableau de décision : tubes nommés / TCP / gRPC / mémoire partagée / COM
· Mis à jour le: · Go Komura · Communication inter-processus, Tubes nommés, Windows, .NET, C#, gRPC, Mémoire partagée, COM, Conception, Tableau de décision, Conseil technique
« Si l’on sépare l’UI et le service, comment les faire communiquer entre eux ? » « Je veux qu’une application 32 bits utilise les fonctionnalités d’une DLL 64 bits. » « Je veux faire transiter des données d’un moteur de mesure tournant dans un processus séparé jusqu’à l’écran. » Scinder une application en plusieurs processus est une réponse pragmatique fréquente à la robustesse, à la séparation des privilèges et aux problèmes de largeur de bits — mais dès l’instant où l’on effectue cette séparation, la question de savoir comment faire communiquer les morceaux se pose inévitablement.
Sur ce blog, nous avons déjà écrit sur des moyens de communication pris individuellement : les pièges de la mémoire partagée, le framing en TCP, la gestion sûre des processus enfants, et l’intégration par fichiers et le verrouillage. Ce qui manquait encore, c’était la vue d’ensemble : quel moyen choisir en premier lieu ? Cet article organise les principales options de communication inter-processus (IPC) sous Windows — intégration par fichiers, tubes nommés, TCP local, gRPC, mémoire partagée et COM — selon leurs points forts et leurs pièges, sous la forme du tableau de décision habituel de ce blog.
1. La conclusion d’abord
- Pour une communication requête-réponse sur la même machine (envoyer une commande et recevoir un résultat), les tubes nommés sont le premier choix. Ils sont natifs de l’OS, ne nécessitent aucun port, s’intègrent au contrôle d’accès de Windows, et s’écrivent simplement en .NET via
System.IO.Pipes.1 - S’il existe une possibilité future de communication à travers le réseau, optez dès le départ pour le TCP local ou gRPC. Migrer d’un tube vers un socket plus tard tend à demander bien plus qu’« un petit ajustement » — c’est une vraie refonte. Notez que le TCP nu n’est qu’un flux d’octets, donc concevoir un mécanisme de framing est indispensable.
- Quand le nombre de services ou de types d’appels augmente et que maintenir un protocole maison devient un fardeau, tournez-vous vers gRPC. Vous obtenez la définition de schéma et la génération de code via les fichiers proto, ainsi que le streaming bidirectionnel, et depuis .NET 8, ASP.NET Core (Kestrel) peut utiliser les tubes nommés comme transport.2
- Réservez la mémoire partagée aux données volumineuses et à haute fréquence (images, formes d’onde). C’est le plus rapide, mais vous devrez concevoir vous-même toute la synchronisation — la règle d’or est de ne jamais faire transiter les messages de contrôle par la mémoire partagée.3
- Pour une intégration faiblement couplée et asynchrone où vous voulez conserver une trace d’audit, l’intégration par fichiers reste un choix solide aujourd’hui. Mais sa réussite dépend entièrement de la conception du contrôle d’exclusion mutuelle.
- COM (hors processus) n’est pas le premier choix pour une conception nouvelle, mais il reste un outil bien vivant dans le contexte de l’utilisation depuis VBA ou d’autres langages, ou comme pont 32 bits/64 bits.4
- Quel que soit le moyen choisi, vous ne pourrez pas échapper aux défis de conception communs que sont les limites de message, les numéros de version, les délais d’expiration et la reconnexion (chapitre 6). Consacrez-y autant de temps qu’au choix du moyen lui-même.
2. Profil de chaque option
2.1 Intégration par fichiers ── faiblement couplée, asynchrone, auditable
C’est le schéma classique : « A dépose un fichier dans un dossier de sortie, B le récupère et le traite. » Les deux parties n’ont pas besoin d’être actives en même temps, le traitement laisse une trace sous forme de fichiers, et en cas d’incident, un humain peut regarder directement le fichier, le corriger et le réinjecter. Pour une intégration de type batch qui n’exige pas de réactivité temps réel, c’est encore aujourd’hui une approche puissante.
Le piège se résume presque entièrement à un seul point : le contrôle d’exclusion mutuelle. « Lire un fichier encore en cours d’écriture » et « deux traitements qui se disputent le même fichier » sont des accidents classiques ; il faut appliquer des pratiques éprouvées comme écrire sous un nom de fichier temporaire puis le renommer. Les détails sont rassemblés dans « Bonnes pratiques d’intégration par fichiers et de verrouillage » — consultez-les impérativement si vous choisissez cette approche. Elle ne convient pas à une communication interactive qui attend une réponse, ni à des échanges se produisant des dizaines de fois par seconde.
2.2 Tubes nommés ── la référence pour l’IPC sur une même machine
Les tubes nommés sont un canal de communication bidirectionnel fourni par le noyau de Windows, et constituent la référence pour l’IPC de type client-serveur sur une même machine. En .NET, on les manipule via NamedPipeServerStream / NamedPipeClientStream, et plusieurs clients peuvent se connecter à un même nom de tube.5 Contrairement à TCP, aucun numéro de port n’est à gérer, et ils ne se heurtent pas aux pare-feux.
Voici les points à retenir en pratique.
- Le mode message est disponible. Spécifier
PipeTransmissionMode.Messagefait que chaque écriture est livrée avec une limite de message bien délimitée. C’est un avantage pratique important : l’OS prend en charge le framing (préfixe de longueur, etc.) qui est obligatoire avec TCP (le côté récepteur doit tout de même lire un message jusqu’au bout viaIsMessageComplete; voir le chapitre 5). - Les droits d’accès par défaut sont étonnamment permissifs. Le descripteur de sécurité par défaut d’un tube donne le contrôle total à LocalSystem, aux administrateurs et au créateur, mais il accorde aussi la lecture à Everyone et aux comptes anonymes.6 Pour une communication entre processus d’un même utilisateur, il suffit d’ajouter
PipeOptions.CurrentUserOnlypour imposer « ne se connecter qu’à un pair créé par le même utilisateur » — je recommande d’en faire votre pratique par défaut.7 Entre comptes différents (communication avec un service, par exemple), définissez explicitement une ACL viaPipeSecurity. - Les noms de tubes vivent dans un espace de noms visible de tous. Les noms de tubes se trouvent dans un unique espace de noms sous
\\.\pipe\, visible aussi par les processus d’autres utilisateurs sur la même machine. Ce à quoi il faut faire attention ici, c’est le squattage de nom. Si un processus malveillant établit en premier un serveur sous le même nom et se met en attente, le client s’y connectera. Sur une machine partagée par des utilisateurs de niveaux de privilège différents, il faut mettre en place des contre-mesures : côté serveur, spécifierPipeOptions.FirstPipeInstancepour échouer si un tube du même nom existe déjà ; côté client, vérifier le compte propriétaire du tube après connexion.8 - Une communication peut franchir une frontière de privilèges. C’est aussi un canal classique entre une « UI en privilèges standards + un broker en privilèges administrateur » séparés par l’UAC. Notez cependant que
CurrentUserOnlyne peut pas être utilisé dans cette configuration (car il exige que le niveau d’élévation corresponde aussi, même pour un même utilisateur7). Il faut concevoir l’ACL explicitement — les détails concrets sont couverts dans « Séparer les privilèges administrateur (Broker) ».
Les points faibles sont qu’ils sont, dans les faits, peu adaptés à la communication entre machines (possible en théorie, mais avec de nombreuses contraintes opérationnelles), et qu’ils sont difficiles à utiliser pour interopérer avec des systèmes non-Windows. Si ces besoins se profilent, choisissez plutôt TCP / gRPC.
2.3 TCP local ── une polyvalence qui traverse langages et OS
Une connexion TCP vers localhost est l’IPC le plus universel qui soit — on peut le parler depuis à peu près n’importe quel langage, runtime ou OS. Dans une configuration mixte comme « un moteur d’analyse sous Linux et une UI sous Windows », TCP (ou HTTP/gRPC par-dessus) est le premier candidat.
Trois points de vigilance :
- C’est un flux d’octets. TCP ne garantit pas « arrive dans les mêmes unités que ce qui a été envoyé ». Il est parfaitement normal que trois messages envoyés via
Sendarrivent concaténés en un seulReceive, ou qu’un seul message arrive fragmenté sur plusieurs lectures. Il faut concevoir soi-même le framing (par exemple un préfixe de longueur) au niveau applicatif — un code qui omet cela « fonctionne par hasard », rien de plus. Voir « L’idée reçue selon laquelle on peut recevoir en TCP dans les mêmes unités qu’on a envoyées » pour plus de détails. - Restreignez la portée exposée par votre écoute. Même si vous ne visez qu’une communication sur la même machine, écouter sur
0.0.0.0permet à d’autres machines du réseau de se connecter. Pour de l’IPC local, la règle est de se lier à127.0.0.1(loopback). Même ainsi, un autre utilisateur de la même machine peut encore se connecter, donc ajoutez une authentification au niveau applicatif si vous devez vérifier l’identité du pair. C’est une différence nette par rapport aux tubes, qui bénéficient d’un contrôle d’accès intégré à l’OS. - La gestion du port vous suit partout. Un port fixe peut entrer en conflit avec un autre logiciel, et certains produits de pare-feu peuvent le détecter comme une « écoute suspecte ». Prévoyez dès la conception un moyen de changer le numéro de port.
2.4 gRPC ── acheter le schéma et la génération de code
Comparé à un simple socket plus un protocole maison, ce que gRPC apporte, ce n’est pas tant la communication elle-même qu’un cadre de développement. En définissant les services et les messages dans un fichier proto, la sérialisation, le framing et le code client/serveur sont tous générés automatiquement, et le streaming bidirectionnel (notifications push depuis le serveur) peut s’écrire comme une fonctionnalité native du langage. Dès que l’on entre dans la phase où « chaque nouveau message oblige à retoucher l’instruction switch du protocole maison et sa documentation », ce cadre devient payant.
Depuis .NET 8, ASP.NET Core (Kestrel) prend en charge directement, en plus de TCP, les sockets de domaine Unix et les tubes nommés comme transports.8 Côté serveur, il suffit d’appeler ListenNamedPipe, et l’on peut aussi configurer le contrôle d’accès via PipeSecurity.2 La combinaison « le canal de communication reste un tube, sans port et intégré aux ACL, tandis que la couche protocole est gRPC » est une option solide pour la séparation UI + service (chapitre 4).
En revanche, il tend à être surdimensionné (overkill) pour la communication entre petits outils. Monter un serveur gRPC signifie porter le socle d’hébergement d’ASP.NET Core, ce qui augmente les livrables, les dépendances et le coût de démarrage. Pour une paire d’outils qui n’échangent que deux ou trois types de commandes, gardez en tête que les tubes nommés + JSON (chapitre 5) seront globalement moins coûteux. Il n’est pas trop tard pour basculer une fois que l’une de ces conditions est vraie : « le nombre de messages à écrire dans le proto a dépassé dix », « le streaming de notifications est réellement indispensable », ou « le pair n’est pas en .NET ».
2.5 Mémoire partagée (fichiers mappés en mémoire) ── la plus rapide, mais toute la synchronisation est à votre charge
L’IPC le plus rapide au sein d’une même machine est la mémoire partagée. En .NET, MemoryMappedFile.CreateNew crée un bloc de mémoire partagée nommé que plusieurs processus peuvent lire et écrire directement comme la même séquence d’octets.3 Sans copie ni sérialisation en travers du chemin, elle offre une performance qui en fait quasiment le seul choix raisonnable pour des données « volumineuses et rapides » comme des images ou des formes d’onde.
Cependant, la mémoire partagée n’est pas « un tube plus rapide ». Vous n’obtenez que les mêmes octets visibles des deux côtés — pas le moindre octet de synchronisation n’est fourni. Un mécanisme pour ne pas lire des données en cours d’écriture, la détection de la vie ou de la mort du pair, et la récupération après un plantage anormal de l’un des deux côtés — tout cela est à concevoir soi-même. La discussion de conception complète, incluant les configurations en tampon circulaire et la gestion des versions de la disposition mémoire, est rassemblée dans « Pièges de la mémoire partagée et bonnes pratiques ».
L’usage pratique est clair : placer uniquement le plan de données (data plane) sur la mémoire partagée, et faire passer le plan de contrôle (control plane) — démarrage/arrêt/changements de configuration — par un canal séparé comme un tube nommé (configuration 3 du chapitre 4). Si vous vous surprenez à vouloir faire transiter même les messages de contrôle par la mémoire partagée, c’est le signe qu’il faut revoir la conception.
2.6 COM (hors processus) ── pas le premier choix pour du neuf, mais toujours utile dans certains contextes
Les serveurs COM hors processus (serveurs EXE) sont le mécanisme de longue date de Windows permettant « d’appeler un objet d’un autre processus comme s’il s’agissait d’une fonction locale ».4 Ce n’est plus le premier choix pour une nouvelle communication entre applications, mais il reste pratique dans les contextes suivants :
- Pont 32 bits/64 bits : une application 32 bits ne peut pas charger une DLL 64 bits dans son propre processus (et inversement), mais COM hors processus permet de franchir cette frontière. L’infrastructure COM prenant en charge le marshaling, le code appelant reste presque inchangé. Un exemple concret est présenté dans « Étude de cas d’un pont COM 32 bits → 64 bits ».
- Utilisation depuis VBA ou d’autres langages : lorsqu’on veut faire appeler des fonctionnalités .NET depuis un environnement d’exécution ancien comme Excel VBA, exposer ces fonctionnalités en COM reste aujourd’hui la voie la plus directe.
- Interopérabilité avec des actifs COM existants : si votre interlocuteur ne sait parler que COM, parler COM vous-même est le chemin le plus court (pour une présentation de COM lui-même, voir « Que sont COM, ActiveX et OCX ? »).
Les raisons d’hésiter à l’adopter pour du neuf sont la lourdeur d’une distribution impliquant l’enregistrement dans le registre, le coût d’apprentissage de la conception d’interfaces et du comptage de références, et la difficulté d’investigation en cas de problème. Avant de trancher, comparez-le à l’alternative « si l’objectif est un pont 32/64, faites simplement du côté 64 bits un simple processus auxiliaire qui parle par tube nommé » (configuration 2 du chapitre 4).
2.7 Mécanismes classiques ── à ne pas choisir pour du neuf
Windows dispose aussi d’autres mécanismes d’IPC — WM_COPYDATA (envoyer des données via un message de fenêtre), le presse-papiers, DDE, ou les mailslots — et ils figurent encore aujourd’hui sur la page officielle de présentation de l’IPC.4 Cependant, soit ils présupposent l’existence d’une fenêtre, soit ils sont en voie d’abandon (les mailslots distants sont en cours de dépréciation), si bien qu’il n’y a pratiquement aucune raison de les choisir pour une conception nouvelle. Il suffit de les reconnaître si vous les croisez en maintenant une application existante.
3. Tableau de décision
| Aspect | Fichier | Tube nommé | TCP local | gRPC | Mémoire partagée | COM |
|---|---|---|---|---|---|---|
| Portée de communication | Peut franchir les machines via un dossier partagé | Pratiquement limité à la même machine | Peut franchir les machines | Peut franchir les machines | Limité à la même machine | Pratiquement limité à la même machine |
| Modèle de communication | Remise de fichiers (asynchrone) | Flux + mode message | Flux d’octets | RPC + streaming | État partagé | Appels de méthode |
| Notification serveur → client | ✕ (polling) | ○ | ○ | ◎ (streaming bidirectionnel) | △ (nécessite un mécanisme d’événement séparé) | △ (possible mais complexe) |
| Franchissement de frontière de privilèges (UAC, services) | ○ (ACL de dossier) | ◎ (PipeSecurity) | △ (authentification à votre charge) | △–○ (◎ avec le transport par tube) | ○ (ACL possible, conception difficile) | ○ |
| 32/64 bits et langages mixtes | ◎ | ○ | ◎ | ◎ (proto génère du code pour chaque langage) | △ (conception ABI obligatoire) | ○ (excelle pour les ponts) |
| Coût d’implémentation | Faible | Faible à moyen | Moyen (framing à votre charge) | Moyen (adoption d’un socle) | Élevé | Élevé (pour du neuf) |
| Facilité de débogage | ◎ (le contenu reste sous forme de fichier) | ○ | ○ (capturable) | △ (HTTP/2 + binaire) | △ (les symptômes sont spectaculaires) | △ |
| Débit / latence | Faible | Moyen à élevé | Moyen | Moyen | ◎ | Moyen |
Une remarque : la bonne approche n’est pas de choisir « le moyen qui a le plus de ◎ », mais de ne regarder que les lignes qui comptent pour vos besoins et de restreindre le choix par élimination. Par exemple, dès l’instant où l’on sait qu’il s’agit de « 30 images par seconde », le plan de données n’a plus qu’un seul choix possible : la mémoire partagée.
4. Exemples de configurations types
Quand on applique le tableau de décision à des besoins concrets, on retombe en pratique le plus souvent sur les trois configurations suivantes.
Configuration 1 : séparer une application UI et un service Windows. Placez dans le service le traitement résident ou nécessitant des privilèges, et gardez l’UI comme un processus utilisateur ordinaire (pour la construction du côté service, voir l’article compagnon publié le même jour, « Article sur les services Windows »). Les tubes nommés sont le moyen de référence pour la communication ; démarrer le protocole avec des messages JSON en mode message (ou en mode octet avec un préfixe de longueur) est un choix solide, et il existe une voie pour migrer vers le transport par tube nommé de gRPC à mesure que la variété des appels grandit.2 Le service tournant sous un compte différent (comme LocalService), CurrentUserOnly ne peut pas être utilisé — le point clé est de définir explicitement une ACL via PipeSecurity, du type « autoriser Users en lecture/écriture, refuser à distance ».
Configuration 2 : pont d’une application 32 bits vers une fonctionnalité 64 bits. Lorsque vous voulez utiliser depuis une application 32 bits existante une DLL ou un SDK de pilote qui ne fonctionne qu’en 64 bits, extrayez le côté 64 bits dans un processus séparé. Deux implémentations sont possibles : (a) monter un processus auxiliaire 64 bits et lui parler par tube nommé, ou (b) en faire un serveur COM hors processus 64 bits. Si le côté appelant est VBA ou un langage ancien, (b) est le choix naturel ; si les deux côtés sont en C#, (a) est plus simple aussi bien à distribuer qu’à investiguer. Pour (a), réutilisez tel quel le modèle de Job Object décrit dans « Gérer les processus enfants en toute sécurité » pour le démarrage, la surveillance et la terminaison coordonnée parent-enfant du processus auxiliaire.
Configuration 3 : données à haute fréquence d’un moteur de mesure vers un affichage. Dans une configuration où un moteur de mesure ou de traitement d’image tourne dans un processus séparé et fait transiter des données vers l’UI, le schéma standard est une conception à deux canaux : le contrôle (démarrage, arrêt, configuration) par tube nommé, et les données (images, formes d’onde) par mémoire partagée + événement nommé. Le contrôle n’ayant lieu que quelques fois par seconde, un tube suffit largement ; les données, elles, s’approchent d’une copie zéro grâce à la mémoire partagée. En séparant les canaux, on peut optimiser la conception du côté données (tampon circulaire, par exemple) indépendamment des contraintes du contrôle. Voir aussi « Afficher l’état des équipements externes » pour l’affichage de l’état du moteur ou des équipements externes.
5. Exemple d’implémentation ── serveur et client asynchrones par tube nommé
Voici une configuration minimale en .NET 8 qui sert de base à une implémentation par tube nommé, incluant le mode message, la prise en charge de plusieurs clients et la gestion des déconnexions. Le protocole est « un message JSON en UTF-8 par écriture », et il porte toujours un champ version. L’exemple ci-dessous suppose une communication entre processus d’un même utilisateur et utilise donc CurrentUserOnly. Si vous communiquez avec un service (compte différent) comme dans la configuration 1, retirez CurrentUserOnly et définissez explicitement une ACL via PipeSecurity, comme indiqué plus loin.
Commençons par le côté serveur. Il recrée un nouveau flux serveur à chaque connexion acceptée, et délègue le traitement de chaque client à sa propre tâche.
using System.IO.Pipes;
using System.Text.Json;
public sealed class PipeServer(string pipeName)
{
// Valeurs par défaut Web : camelCase et insensible à la casse. Les valeurs
// par défaut de System.Text.Json sont sensibles à la casse ; sans ce
// partage, "version" ne se lierait pas à Request.Version et une requête
// valide tomberait dans unknown_type
internal static readonly JsonSerializerOptions JsonOptions =
new(JsonSerializerDefaults.Web);
public async Task RunAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var pipe = new NamedPipeServerStream(
pipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Message, // une écriture = un message
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
await pipe.WaitForConnectionAsync(ct);
_ = Task.Run(() => HandleClientAsync(pipe, ct), ct); // isole chaque connexion
}
}
// Limite supérieure pour un seul message. Protège la mémoire du service
// même si le pair envoie un message surdimensionné (le pair IPC est lui
// aussi une entrée externe ; voir le chapitre 6)
private const int MaxMessageBytes = 1024 * 1024;
private static async Task HandleClientAsync(
NamedPipeServerStream pipe, CancellationToken ct)
{
await using (pipe)
{
var buffer = new byte[64 * 1024];
try
{
while (!ct.IsCancellationRequested)
{
// Même en mode message, une seule lecture (Read) ne garantit
// pas de lire un message entier. Continuez à lire jusqu'à
// IsMessageComplete
using var ms = new MemoryStream();
do
{
int n = await pipe.ReadAsync(buffer, ct);
if (n == 0) return; // le client s'est déconnecté
ms.Write(buffer, 0, n);
if (ms.Length > MaxMessageBytes) return; // dépassement de la limite : déconnexion
} while (!pipe.IsMessageComplete);
byte[] response = Dispatch(ms.ToArray());
await pipe.WriteAsync(response, ct);
}
}
catch (IOException)
{
// Seul le canal avec ce client s'est rompu.
// N'arrêtez pas tout le serveur : continuez à servir les autres connexions
}
}
}
private static byte[] Dispatch(byte[] payload)
{
Request? req;
try { req = JsonSerializer.Deserialize<Request>(payload, JsonOptions); }
catch (JsonException) { req = null; }
// Rejetez ici les requêtes mal formées, les versions inconnues et les types inconnus
object result = req switch
{
null => new { version = 1, error = "bad_request" },
// Une version non spécifiée (liée à 0) ou une version inconnue est rejetée ici
{ Version: not 1 } => new { version = 1, error = "version_unsupported" },
{ Type: "getStatus" } => new { version = 1, running = true },
{ Type: "startJob" } => new { version = 1, jobId = StartJob(req) },
_ => new { version = 1, error = "unknown_type" },
};
return JsonSerializer.SerializeToUtf8Bytes(result, JsonOptions);
}
}
public sealed record Request(int Version, string Type, JsonElement? Body);
Le côté client encapsule « connexion → requête → réponse » dans une seule méthode, avec un délai d’expiration et des tentatives de nouvelle requête intégrés dès le départ.
using System.IO.Pipes;
using System.Text.Json;
public sealed class PipeClient(string pipeName)
{
// idempotent : ne réessaie que lorsque l'appelant a déclaré que cette
// requête peut être reçue deux fois sans danger. La valeur par défaut
// est « ne pas réessayer »
public async Task<TResponse?> RequestAsync<TResponse>(
object request, CancellationToken ct, bool idempotent = false)
{
for (int attempt = 1; ; attempt++)
{
// Fixe une limite de temps non seulement pour la connexion, mais pour
// tout l'échange requête → réponse. Cela évite une attente infinie
// si le serveur accepte la connexion mais reste bloqué sans écrire
// de réponse
using var deadline =
CancellationTokenSource.CreateLinkedTokenSource(ct);
deadline.CancelAfter(TimeSpan.FromSeconds(10));
try
{
using var pipe = new NamedPipeClientStream(
".", pipeName, PipeDirection.InOut,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
// N'attend pas indéfiniment. Si le serveur n'est pas démarré, cela lève une exception
await pipe.ConnectAsync(timeout: 3000, deadline.Token);
pipe.ReadMode = PipeTransmissionMode.Message;
await pipe.WriteAsync(
JsonSerializer.SerializeToUtf8Bytes(
request, PipeServer.JsonOptions),
deadline.Token);
using var ms = new MemoryStream();
var buffer = new byte[64 * 1024];
do
{
int n = await pipe.ReadAsync(buffer, deadline.Token);
if (n == 0) throw new IOException("Le serveur s'est déconnecté.");
ms.Write(buffer, 0, n);
} while (!pipe.IsMessageComplete);
return JsonSerializer.Deserialize<TResponse>(
ms.ToArray(), PipeServer.JsonOptions);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw; // annulation par l'appelant : ne pas réessayer
}
catch (Exception ex) when (
ex is IOException or TimeoutException or OperationCanceledException
&& idempotent && attempt < 3)
{
// Réessaie les échecs transitoires, par exemple pendant un redémarrage
// du serveur. Dans le cas « la requête a été envoyée, mais la
// connexion s'est rompue avant de lire la réponse », la requête
// a peut-être déjà été exécutée : les requêtes avec effets de
// bord (comme startJob) ne sont donc pas réessayées par défaut.
// Concevez-les d'abord pour rejeter les doublons via un ID de
// requête, puis passez idempotent: true (chapitre 6)
await Task.Delay(500 * attempt, ct);
}
}
}
}
Trois décisions de conception dans ce code méritent une précision.
- Incluez
versiondès la toute première version livrée. Il y aura toujours un moment où l’UI et le service seront mis à jour séparément (l’un devenant plus récent que l’autre). Le simple fait que le côté récepteur « rejette explicitement toute version qu’il ne connaît pas » transforme un dysfonctionnement silencieux en une erreur compréhensible. - Décidez si les connexions sont jetables ou persistantes. L’exemple ci-dessus utilise le style jetable — se connecter à chaque requête —, ce qui évite d’avoir à gérer l’état de déconnexion/reconnexion, au prix d’une inadéquation aux appels à haute fréquence. Le jour où vous aurez besoin d’une connexion persistante avec des notifications push venant du serveur, cette complexité supplémentaire est un bon signal pour basculer vers le streaming bidirectionnel de gRPC.
- Pour communiquer avec un service, retirez
CurrentUserOnlyet concevezPipeSecurity. L’exemple ci-dessus suppose une communication entre processus d’un même utilisateur. Si le pair est un service tournant sous un compte différent, créez le flux serveur avec une ACL viaNamedPipeServerStreamAcl.Create, et indiquez explicitement quel groupe est autorisé à se connecter.6
6. Points d’attention communs à la conception du protocole ── le travail qui reste une fois le moyen choisi
Quel que soit l’IPC choisi, la conception du contenu de la communication soulève des défis communs. En réalité, ceux-ci sont une source d’incidents encore plus importante que le choix du moyen lui-même.
- Limites de message (framing) : sauf lorsque l’OS délimite les messages pour vous, comme avec le mode message des tubes, « où commence et où finit un message » est de la responsabilité de l’application. Les limites d’enregistrement en mémoire partagée posent le même problème. Adoptez par défaut un schéma à préfixe de longueur.
- Gestion des versions : incluez une version de format dans chaque message, et faites en sorte que le côté récepteur « rejette explicitement les versions inconnues ». Rien ne garantit que les deux processus seront toujours mis à jour en même temps, même s’ils sont distribués via le même installeur.
- Délais d’expiration : concevez en partant du principe que « le pair ne répond pas » finira toujours par arriver. Fixez une limite de temps pour chacune des étapes — connexion, requête, réponse — et ne laissez aucun code en attente infinie. Attendre de façon synchrone sur le thread d’UI est totalement à exclure.
- Reconnexion et idempotence : réessayer, c’est créer la possibilité qu’une même requête arrive deux fois. Classez chaque type de requête selon qu’il est « sûr à exécuter deux fois » ou non, et pour celles qui ne le sont pas (comme la soumission d’un job), attachez un identifiant de requête pour rejeter les doublons.
- Traitez aussi le pair comme une entrée externe : pour une communication au sein d’une même machine, on a tendance à négliger la validation en se disant que « le pair est notre propre application » — mais dès lors qu’une frontière de privilèges est en jeu, le pair IPC est une entrée non fiable, tout comme ce qui arrive du réseau. Un broker ou un service en privilèges administrateur ne doit jamais exécuter tel quel un chemin ou une commande reçus d’un client en privilèges standards. Soupçonner à la fois « qui » et « quoi » — y compris le squattage de nom de tube (section 2.2) — est la discipline propre à l’IPC qui franchit une frontière de privilèges.
- Conservez un moyen d’observation : pouvoir émettre des journaux de communication (au minimum le type de message, le pair et le résultat) change d’un ordre de grandeur le temps d’investigation des problèmes du type « ça ne se connecte pas » ou « pas de réponse ».
7. Conclusion
On se trompe rarement sur le choix de la communication inter-processus si l’on réfléchit dans cet ordre. D’abord, demandez-vous si se limiter à une même machine est acceptable. Si oui : tubes nommés pour le requête-réponse, mémoire partagée seulement pour les données volumineuses à haute fréquence, et intégration par fichiers pour le batch faiblement couplé. Si franchir les machines ou mélanger les langages se profile, optez pour le TCP local ou gRPC, et choisissez gRPC dès que gérer un protocole maison devient un fardeau à l’échelle. Gardez COM en réserve comme outil pour les ponts 32/64 bits et l’interopérabilité VBA plutôt que comme premier choix pour du neuf — le tableau de décision de cet article n’est que cette logique mise en forme.
Et une fois le moyen choisi, veillez à intégrer dès la première version livrée les préoccupations communes du chapitre 6 — framing, version, délais d’expiration, reconnexion et validation du pair. Sur le terrain, on constate que les incidents d’IPC sont bien plus souvent causés par « l’omission de la conception du protocole » que par « une erreur dans le choix du moyen ». Pour revoir un découpage en processus ou un mécanisme de communication, je recommande de commencer par un inventaire de « quels processus, sous quels privilèges, échangent quoi, et à quelle fréquence ».
Articles connexes
- Pièges de la mémoire partagée et bonnes pratiques
- L’idée reçue selon laquelle on peut recevoir en TCP dans les mêmes unités qu’on a envoyées ── concevoir la réception en traitant TCP comme un flux d’octets
- Séparer les privilèges administrateur (Broker) ── concevoir à travers la frontière UAC
- Comment créer un service Windows ── guide du Worker Service
Domaines de consultation connexes
Komura Software LLC prend en charge la revue de conception du découpage en processus et des mécanismes de communication, la conception de l’intégration avec des actifs existants — y compris les ponts 32 bits/64 bits — ainsi que l’investigation des causes profondes des problèmes de communication du type « ça ne se connecte pas » ou « pas de réponse ».
- Consultation technique et revue de conception
- Développement d’applications Windows
- Réutilisation des actifs existants et accompagnement à la migration
- Contact
Références
-
Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication. Sur un exemple d’implémentation de serveur et de client prenant en charge plusieurs clients via NamedPipeServerStream / NamedPipeClientStream. ↩
-
Microsoft Learn, Inter-process communication with gRPC and Named pipes. Sur la configuration permettant de faire fonctionner gRPC au-dessus des tubes nommés via ListenNamedPipe de Kestrel depuis .NET 8, et sur le contrôle d’accès via PipeSecurity. ↩ ↩2 ↩3
-
Microsoft Learn, MemoryMappedFile Class. Sur l’API .NET des fichiers mappés en mémoire, et sur le fait que la mémoire partagée créée avec CreateNew, non liée à un fichier, est bien adaptée à un usage IPC. ↩ ↩2
-
Microsoft Learn, Interprocess communications. Sur la liste des mécanismes d’IPC fournis par Windows (presse-papiers, COM, WM_COPYDATA, DDE, mappage de fichiers, mailslots, tubes, RPC, sockets Windows) et les indications sur leurs usages respectifs. ↩ ↩2 ↩3
-
Microsoft Learn, NamedPipeServerStream Class. Sur le flux côté serveur des tubes nommés, et sur les constructeurs permettant de spécifier PipeTransmissionMode ou PipeSecurity. ↩
-
Microsoft Learn, Named Pipe Security and Access Rights. Sur le fait que le descripteur de sécurité par défaut d’un tube nommé accorde la lecture à Everyone et aux comptes anonymes, et sur la structure des droits d’accès. ↩ ↩2
-
Microsoft Learn, PipeOptions Enum. Sur le fait que CurrentUserOnly n’autorise la connexion qu’à un pair « créé par le même utilisateur », et que sous Windows, il vérifie aussi le niveau d’élévation en plus du compte utilisateur. ↩ ↩2
-
Microsoft Learn, Inter-process communication with gRPC. Sur les transports (sockets de domaine Unix, tubes nommés) utilisés pour employer gRPC comme IPC, et sur les considérations de sécurité telles que la vérification du propriétaire du serveur et les contre-mesures contre l’usurpation d’identité (impersonation). ↩ ↩2
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Empêcher les lancements multiples d'une application Windows — Mutex nommé et activation de la fenêtre existante lors d'un second lancement
Cet article détaille comment implémenter la prévention des lancements multiples d'une application Windows métier à l'aide d'un Mutex nomm...
Comment créer et exploiter un service Windows ── du choix entre Planificateur de tâches et service à la transformation d'un BackgroundService en service Windows
Faut-il transformer un traitement résident en service Windows, ou le Planificateur de tâches suffit-il ? Ce guide organise, du point de v...
Comment choisir où une application Windows stocke ses données locales — tableau de décision pour SQLite / JSON / Registre / Access
Où et sous quelle forme une application de bureau Windows doit-elle stocker ses données ? Cet article organise le choix entre AppData et ...
Compatibilité descendante des interfaces DLL et COM — Tableau de décision : quels changements cassent les appelants
Quels changements apportés à une DLL ou à un composant COM cassent réellement leurs appelants ? Nous détaillons les trois niveaux de comp...
Modifier en toute sécurité une application métier legacy sans tests — Tests de caractérisation et refactorisation en pratique
Pour modifier en toute sécurité une application métier sans tests, cet article explique, avec des exemples en C#, la procédure du test de...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Migration ActiveX
Choisir de conserver, encapsuler ou remplacer des composants COM / ActiveX / OCX.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Réutilisation et migration d'actifs existants
Réutilisation et migration d'actifs COM / ActiveX / OCX et de dépendances 32 ou 64 bits.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Quel doit être le premier choix pour la communication inter-processus sous Windows ?
- Pour une communication requête-réponse sur une même machine (envoyer une commande et recevoir un résultat), les tubes nommés sont le premier choix. Ils sont natifs de l'OS, ne nécessitent aucune gestion de numéro de port, ne se heurtent pas aux pare-feux, s'intègrent au contrôle d'accès de Windows (PipeSecurity), et s'écrivent simplement en .NET via System.IO.Pipes. La bonne méthode de sélection consiste à restreindre par élimination : optez dès le départ pour le TCP local ou gRPC s'il existe une possibilité future de communication à travers le réseau, réservez la mémoire partagée aux seules données volumineuses et à haute fréquence (images, formes d'onde), et choisissez l'intégration par fichiers pour une intégration faiblement couplée et asynchrone où vous voulez conserver un journal d'audit.
- Dans quels cas faut-il utiliser gRPC pour la communication inter-processus ?
- Quand le nombre de services ou de types d'appels augmente et que maintenir l'instruction switch et la documentation d'un protocole maison devient un fardeau. Vous obtenez alors la définition de schéma et la génération de code via les fichiers proto, ainsi que le streaming bidirectionnel, et depuis .NET 8, ASP.NET Core (Kestrel) prend directement en charge les tubes nommés comme transport. En revanche, pour une paire d'outils qui n'échangent que deux ou trois types de commandes, porter le socle d'hébergement d'ASP.NET Core est surdimensionné, et les tubes nommés associés à JSON reviennent globalement moins cher. Il n'est pas trop tard pour basculer une fois que l'une de ces conditions est vraie : le nombre de messages à écrire dans le proto a dépassé dix, le streaming de notifications est réellement indispensable, ou le pair n'est pas en .NET.
- Comment utiliser une DLL 64 bits depuis une application 32 bits ?
- Une DLL 64 bits ne peut pas être chargée dans le même processus qu'un processus 32 bits, il faut donc extraire le côté 64 bits dans un processus séparé. Deux implémentations sont possibles : (a) monter un processus auxiliaire 64 bits et communiquer par tube nommé, ou (b) en faire un serveur COM hors processus 64 bits. Si le côté appelant est VBA ou un langage ancien, (b) est le choix naturel, car l'infrastructure COM prend en charge le marshaling et le code appelant reste presque inchangé. Si les deux côtés sont en C#, (a) est plus simple aussi bien à distribuer qu'à investiguer. Dans le cas de (a), concevez la terminaison coordonnée parent-enfant du processus auxiliaire avec un Job Object.
- Dans quelles situations faut-il utiliser la mémoire partagée ?
- Réservez-la aux données volumineuses et à haute fréquence, comme des images ou des formes d'onde. C'est l'IPC le plus rapide au sein d'une même machine, sans copie ni sérialisation en travers du chemin, mais comme pas le moindre octet de synchronisation n'est fourni, il vous faudra concevoir vous-même un mécanisme pour ne pas lire des données en cours d'écriture, la détection de la vie ou de la mort du pair, et la récupération après un plantage anormal. En pratique, le schéma standard est une conception à deux canaux : placer uniquement le plan de données (data plane) sur la mémoire partagée, et faire passer le plan de contrôle (control plane) — démarrage, arrêt, changements de configuration — par un canal séparé comme un tube nommé. Si vous commencez à vouloir faire transiter même les messages de contrôle par la mémoire partagée, c'est le signe qu'il faut revoir la conception.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics