Présentation des mailslots et de leur utilisation en Delphi

Pierre Castelain

Cet article a pour but de présenter les mailslots et leur utilisation en Delphi. Les mailslot représentent un moyen simple de faire communiquer différents processus entre eux.

- Titre : Présentation des mailslots et de leur utilisation en Delphi

- Auteur : Pierre Castelain

- Parution : 27 juin 2004

- Mise à jour : 31 décembre 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2004 Pierre Castelain. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les adresses

- III. Les fonctions

- III-A. CreateMailslot

- III-B. GetMailslotInfo

- III-C. SetMailslotInfo

- IV. Utilisation en Delphi

- IV-A. Un exemple simple

- IV-A-1. Le serveur

- IV-A-2. Le client

- IV-B. Allons plus loin

- V. Liens

I - Introduction

Dans votre carrière de développeur, vous serez (si ce n'est pas déjà le cas) confronté à un moment ou un autre au besoin de faire communiquer des applications entre elles. Il existe de nombreuses techniques sous Windows vous permettant de réaliser cette opération. Ces techniques sont souvent réunies sous le nom de Communication InterProcessus (ou IPC en anglais pour Interprocess Communication). Leur but est de faire transiter une certaine quantité d'informations d'un processus à un autre (le plus souvent des applications).

Les principales techniques disponibles sous Windows sont :

- le presse-papier ;

- COM ;

- les messages Windows (et en particulier le message WM_COPYDATA) ;

- DDE ;

- les fichiers mappés (file mapping) ;

- les mailslots ;

- les pipes (anonymes ou nommés) ;

- RPC ;

- les sockets Windows.

Il existe d'autres moyens d'arriver à nos fins, comme l'utilisation de fichiers temporaires ou de la base de registre, mais ce ne sont pas à proprement parler des techniques IPC.

Nous allons nous intéresser aux mailslots. Il s'agit d'une technique très simple d'emploi, mais qui peut rendre de nombreux services.
Pour vous faire une idée plus précise de ce que sont les mailslots, imaginez juste une boîte aux lettres dont seul le propriétaire a la clé. Toutes les personnes connaissant l'adresse de la boîte peuvent y déposer des messages, mais seul le propriétaire peut les lire. C'est tout simplement comme ça que fonctionnent les mailslots.
Nous appellerons le propriétaire de la boîte le processus serveur, et les personnes déposant des messages les processus clients.
Les mailslots sont unidirectionnels, c'est-à-dire que la communication ne s'effectue que dans un seul sens : du client vers le serveur. Ceci dit, rien ne vous empêche d'utiliser dans la même application un mailslot pour l'envoi et un autre pour la réception.
Les mailslots sont asynchrones, c'est-à-dire que le client dépose son message et que le serveur le lit quand il le désire ou quand il en a le temps. Il n'y a donc aucun moyen de savoir si un message a été lu ou non.
Un message ne peut dépasser la taille de 64 Ko (ou 425 octets en broadcast, voir plus loin). Si vous voulez faire transiter des masses de données plus importantes, tournez-vous vers une autre technique.
Si vous trouvez que c'est un mauvais début, attendez un peu…

Les mailslots présentent certains avantages :

- ils ne nécessitent qu'un jeu de fonctions très réduit et simple à utiliser ;

- ils permettent de faire communiquer simplement des processus s'exécutant sur différents ordinateurs à travers un réseau ;

- ils intègrent également un mécanisme de diffusion qui permet à un client d'envoyer un message à tous les serveurs d'un domaine en un seul appel.

Vous trouvez que cela redevient intéressant ? Alors, continuons.

II - Les adresses

Avant de découvrir en détail l'utilisation des mailslots, penchons-nous un moment sur la syntaxe des adresses.
Les adresses des mailslots doivent être construites en suivant certaines règles simples. Ces règles sont communes au client et au serveur, mais vous n'emploierez pas toujours la même forme pour un client que pour un serveur. La forme générale d'une adresse de mailslot est la suivante :

Les parties apparaissant en bleu sont obligatoires et ne peuvent être modifiées. La partie apparaissant en vert est facultative. Le reste est à votre disposition.
Décomposons cette adresse pour la comprendre :

- une adresse de mailslot commence toujours par un double antislash '\\' ;

- vient ensuite le nom de l'emplacement du mailslot. Cet emplacement peut représenter :

- un ordinateur : nous utiliserons le nom de l'ordinateur dans le réseau,

- tous les ordinateurs d'un domaine : nous utiliserons le nom du domaine (ou du groupe de travail),

- tous les ordinateurs du domaine principal : nous utiliserons le caractère étoile '*',

- l'ordinateur sur lequel le processus s'exécute : nous utiliserons le caractère point '.'.
Attention : seuls les messages de moins de 425 octets peuvent être envoyés à plusieurs ordinateurs en même temps (broadcast) ;

- l'emplacement est toujours suivi de la chaîne '\mailslot\' qui indique qu'il ne s'agit pas d'un « vrai » fichier, mais bien d'un mailslot ;

- la dernière section est constituée d'un certain nombre de répertoires séparés par des antislashs '\' et du nom du mailslot. Cette section est donc construite comme un chemin de fichier classique. Vous pouvez donc classer les mailslots par répertoires si vous en utilisez beaucoup en même temps ou si vous voulez être rigoureux.

Les clients et le serveur devront utiliser la même adresse pour communiquer à une exception près : l'emplacement de création du mailslot par le serveur est toujours l'ordinateur local, donc le point '.'.

Un petit exemple
Imaginons que le serveur s'exécute sur un ordinateur appelé 'SERVEUR' situé sur un domaine appelé 'DOMAINE' et que le mailslot porte le nom de 'TEST'.

- Le mailslot sera créé par le processus serveur avec l'adresse : '\\.\mailslot\TEST'.

- Le mailslot sera ouvert par un client s'exécutant sur la même machine avec l'adresse '\\.\mailslot\TEST'.

- Le mailslot sera ouvert par un client s'exécutant sur une autre machine avec l'adresse '\\SERVEUR\mailslot\TEST'.

- Le mailslot sera ouvert par un client désirant envoyer un message à tous les serveurs du domaine 'DOMAINE' avec l'adresse '\\DOMAINE\mailslot\TEST'.

- Enfin, le mailslot sera ouvert par un client désirant envoyer un message à tous les serveurs du domaine principal avec l'adresse '\\*\mailslot\TEST'.

Sachez enfin que les adresses de mailslots ne sont pas sensibles à la casse. Les adresses '\\.\mailslot\test' et '\\.\MAILSLOT\TEST' sont donc équivalentes.

III - Les fonctions

Les mailslots sont implémentés comme des pseudofichiers. En clair, cela signifie que les fonctions permettant d'envoyer ou de lire des messages sont les mêmes que celles habituellement utilisées pour les fichiers. Seules trois fonctions leur sont spécifiques.

III-A - CreateMailslot

Cette fonction se charge de créer le mailslot pour le serveur.
Voici sa déclaration en Delphi :

function CreateMailslot(lpName: PChar; nMaxMessageSize: DWORD;
lReadTimeout: DWORD; lpSecurityAttributes: PSecurityAttributes): THandle; stdcall;

Les arguments sont :

- lpName : le nom du mailslot (son adresse) ;

- nMaxMessageSize : la taille maximale d'un message (0 pour une taille quelconque) ;

- lReadTimeout : le temps maximal en millisecondes pendant lequel le serveur attendra un nouveau message avant de quitter le processus de lecture. 0 pour un retour immédiat s'il n'y a pas de message et MAILSLOT_WAIT_FOREVER pour une attente infinie ;

- lpSecurityAttributes : un pointeur vers une structure de type SECURITY_ATTRIBUTES que nous ignorerons dans cet article. Sachez simplement que si vous voulez partager le mailslot avec un processus enfant du serveur, vous devrez modifier le membre bInheritHandle de cette structure ;

- la valeur de retour est un handle (valeur numérique) qui sera utilisé comme argument par les autres fonctions afin d'identifier le mailslot.

Un appel type en Delphi sera donc de la forme :

h:= CreateMailslot('\\.\mailslot\MonMailslot', 0, MAILSLOT_WAIT_FOREVER, nil);

III-B - GetMailslotInfo

Cette fonction permet de lire les informations concernant le mailslot.
Voici sa déclaration en Delphi :

function GetMailslotInfo(hMailslot: THandle; lpMaxMessageSize: Pointer;
var lpNextSize: DWORD; lpMessageCount, lpReadTimeout: Pointer): BOOL; stdcall;

Les arguments sont :

- hMailslot : le handle obtenu par l'appel à CreateMailslot ;

- lpMaxMessageSize : pointeur sur un entier qui sera rempli avec la taille maximale d'un message sur ce mailslot (la valeur fournie à la fonction CreateMailslot) ;

- lpNextSize : variable de type entier qui sera remplie avec la taille du prochain message à lire (MAILSLOT_NO_MESSAGE s'il n'y a pas de message à lire) ;

- lpMessageCount : pointeur sur un entier qui sera rempli avec le nombre de messages à lire ;

- lpReadTimeout : pointeur sur un entier qui sera rempli avec le temps maximal en millisecondes pendant lequel le serveur attendra un nouveau message (valeur fournie à la fonction CreateMailProcess) ;

- la valeur de retour est un booléen indiquant si la fonction s'est terminée correctement ou si une erreur est survenue. En cas d'erreur, consultez GetLastError pour plus d'information sur celle-ci.

Une petite remarque : la déclaration de cette fonction en Delphi est un peu étrange, car, en principe, il est possible de passer la valeur nil à tous les arguments sauf évidemment pour hMailslot. Pourtant, la déclaration de l'argument lpNextSize en var interdit ce genre d'appel. Je ne sais pas s'il s'agit d'une erreur ou si c'est volontaire. En tout cas, il nous faudra toujours passer au moins une variable pour cet argument (à moins de redéclarer la fonction :-)

III-C - SetMailslotInfo

Cette fonction permet uniquement de modifier la durée maximale en millisecondes pendant laquelle la lecture attendra un nouveau message. Nous ne nous en servirons pas dans cet article.
Voici sa déclaration en Delphi :

function SetMailslotInfo(hMailslot: THandle; lReadTimeout: DWORD): BOOL; stdcall;

Les arguments sont :

- hMailslot : le handle obtenu par l'appel à CreateMailslot ;

- lReadTimeout : entier dont la valeur correspond au temps maximal en millisecondes pendant lequel le serveur attendra un nouveau message ;

- la valeur de retour est un booléen indiquant si la fonction s'est terminée correctement ou si une erreur est survenue. En cas d'erreur, consultez GetLastError pour plus d'information sur celle-ci.

C'est tout pour les fonctions spécifiques, voyons maintenant la liste des fonctions communes avec la gestion des fichiers.

Pour le serveur :

- CloseHandle ;

- DuplicateHandle ;

- ReadFile et ReadFileEx ;

- GetFileTime ;

- SetFileTime ;

- GetHandleInformation ;

- SetHandleInformation.

Les seules qui nous intéressent réellement sont CloseHandle et ReadFile. Elles vont nous permettre respectivement de fermer le mailslot et de lire les messages qu'il contient.

Pour le client :

- CloseHandle ;

- CreateFile ;

- DuplicateHandle ;

- WriteFile et WriteFileEx.

Nous ne nous intéresserons pas aux fonctions DuplicateHandle (qui permet de copier un handle) et WriteFileEx (qui n'est qu'une version améliorée de WriteFile). Il nous reste donc CloseHandle qui permettra de fermer la communication avec le mailslot, CreateFile qui permettra d'ouvrir la communication avec le mailslot et WriteFile que nous utiliserons pour écrire le message.

Je vous laisse le soin de regarder vous-même les déclarations de ces fonctions.

IV - Utilisation en Delphi

Maintenant que nous savons comment les mailslots fonctionnent, voyons comment les utiliser en Delphi.

IV-A - Un exemple simple

Nous allons créer deux applications et les faire communiquer par l'intermédiaire d'un seul mailslot.

IV-A-1 - Le serveur

Créez une nouvelle application dans Delphi et enregistrez-la sous le nom de 'Serveur'.
Enregistrez la fenêtre sous le nom 'ServeurFrm', cela nous évitera de l'écraser plus tard.
Ajoutez un composant TMemo sur la fenêtre et appelez-le 'mmMessages'.
Ajoutez un composant Tbutton sur la fenêtre et appelez-le 'btnLire'.
Dans la section private de la classe de la fenêtre, ajoutez la ligne suivante :

FHandle: THandle;

Dans l'événement OnCreate de la fenêtre, ajoutez le code suivant :

FHandle:= CreateMailslot('\\.\mailslot\ExempleSimple', 0,
MAILSLOT_WAIT_FOREVER, nil);

Dans l'événement OnDestroy de la fenêtre, ajoutez le code suivant :

CloseHandle(FHandle);

Dans l'événement OnClick du bouton, ajoutez le code suivant :

var
lpNextSize, lpMessageCount, lpNumberOfBytesRead: Cardinal;
buffer: string;
begin
if GetMailslotInfo(FHandle, nil, lpNextSize, @lpMessagecount, nil) and
(lpMessageCount > 0) then
begin
SetLength(buffer, lpNextSize);
if ReadFile(FHandle, buffer[1], lpNextSize, lpNumberOfBytesRead, nil) then
mmMessages.Lines.Add(buffer);
end;
end;

Vous devriez avoir ceci dans l'éditeur :

unit ServeurFrm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

type
TForm1 = class(TForm)
mmMessages: TMemo;
btnLire: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnLireClick(Sender: TObject);
private
{ Private declarations }
FHandle: THandle;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
FHandle:= CreateMailslot('\\.\mailslot\ExempleSimple', 0,
MAILSLOT_WAIT_FOREVER, nil);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
CloseHandle(FHandle);
end;

procedure TForm1.btnLireClick(Sender: TObject);
var
lpNextSize, lpMessageCount, lpNumberOfBytesRead: Cardinal;
buffer: string;
begin
if GetMailslotInfo(FHandle, nil, lpNextSize, @lpMessagecount, nil) and
(lpMessageCount > 0) then
begin
SetLength(buffer, lpNextSize);
if ReadFile(FHandle, buffer[1], lpNextSize, lpNumberOfBytesRead, nil) then
mmMessages.Lines.Add(buffer);
end;
end;

end.

Compilez le tout. Notre serveur est terminé !

Comment fonctionne-t-il ?

- À la création de la fenêtre, nous créons un mailslot dont nous conservons le handle.

- Lors d'un appui sur le bouton, nous regardons si des messages sont disponibles et si c'est le cas, nous lisons un message et le copions dans le TMemo.

- À la destruction de la fenêtre, nous libérons le handle du mailslot, ce qui a pour effet de fermer le mailslot et de le supprimer (ainsi que les messages qu'il pourrait contenir).

IV-A-2 - Le client

Créez un nouveau projet et enregistrez-le sous le nom de 'Client'.
Enregistrez la fenêtre sous le nom de 'ClientFrm'.
Ajoutez un composant TMemo sur la fenêtre et appelez-le 'mmMessage'.
Ajoutez un composant Tbutton sur la fenêtre et appelez-le 'btnEcrire'.
Dans l'événement OnClick du bouton, ajoutez le code suivant :

var
FHandle: THandle;
buffer: PChar;
lpNumberOfBytesWritten: Cardinal;
begin
FHandle:= CreateFile('\\.\mailslot\ExempleSimple', GENERIC_WRITE,
FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if FHandle = INVALID_HANDLE_VALUE then
raise Exception.Create(SysErrorMessage(GetLastError))
else
begin
buffer:= PChar(mmMessage.Lines.Text);
WriteFile(FHandle, buffer^, Length(buffer),
lpNumberOfBytesWritten, nil);
CloseHandle(FHandle);
end;
end;

Compilez le tout. C'est fini pour notre client dont voici le fonctionnement.

- Comme vous pouvez le constater, nous commençons par tenter d'ouvrir le mailslot. Si ce n'est pas possible, la valeur du handle contient la valeur de la constante INVALID_HANDLE_VALUE. Dans ce cas, nous affichons la raison de l'erreur en utilisant les fonctions GetlastError (qui retourne le numéro de la dernière erreur) et SysErrorMessage qui affiche le message correspondant à l'erreur.

- Si tout s'est bien passé, nous écrivons le contenu du TMemo dans le mailslot, en passant par une variable temporaire pour éviter un message d'erreur du compilateur en raison de la forme de la déclaration de la fonction. Nous terminons en fermant le mailslot par un appel à CloseHandle.

Cela peut vous paraître complexe si vous n'êtes pas habitué à l'utilisation des fonctions de l'API Windows. Mais en réalité c'est très simple. Surtout que vous utiliserez toujours cette forme d'appel.

Pour tester vos programmes, lancez le serveur en premier (pour qu'il crée le mailslot) et exécutez ensuite le client. Dans le client, tapez un message et cliquez sur le bouton afin de placer le texte dans le mailslot. Cliquez maintenant sur le bouton du serveur pour lire le message.

IV-B - Allons plus loin

Si le principe de fonctionnement des mailslots vous plait, vous voudrez peut-être disposer de classes Delphi vous permettant de les manipuler plus aisément afin d'éviter les appels direct à l'API Windows. Je vous propose donc deux classes (TMailslotServer et TMailslotClient) simples, mais qui devraient faire l'affaire.
Dans la mesure où un certain nombre de fonctionnalités sont communes entre les serveurs et le client, j'ai dérivé ces deux classes d'une classe ancêtre (TMailslotCommon) implémentant ces fonctionnalités partagées. Ces classes sont décrites dans une unité appelée mailslots.pas dont voici la partie interface:

unit Mailslots;

interface
uses
Classes, Windows;

type
TMailslotCommon = class
private
FMailslotName: string;
procedure SetMailslotName(const Value: string);
function GetConnected: boolean;
procedure SetConnected(const Value: boolean);
procedure SetLocation(const Value: string);
protected
FMSHandle: THandle;
FLocation: string;

procedure RaiseError(Msg: string = '');

property Location: string read FLocation write SetLocation;
public
constructor Create; virtual;
destructor Destroy; override;

function Connect: boolean; virtual; abstract;
procedure Disconnect; virtual;
function GetMailslotAddress: string;

property Connected: boolean read GetConnected write SetConnected;
property MailslotName: string read FMailslotName write SetMailslotName;
end;

TMailslotServer = class(TMailslotCommon)
public
function Connect: boolean; override;
function MessageCount: Cardinal;
function NextMessageSize: Cardinal;
procedure ReadBuffer(var Buffer; BufferSize: Cardinal);
procedure ReadStream(AStream: TStream);
function ReadString: string;
end;

TMailslotClient = class(TMailslotCommon)
public
function Connect: boolean; override;
procedure WriteBuffer(const Buffer; BufferSize: Cardinal);
procedure WriteStream(AStream: TStream);
procedure WriteString(AString: string);

property Location; // rendu accessible pour le client uniquement
end;

Quelques précisions sur les membres de ces classes

- Le client propose les propriétés Location et MailslotName qui correspondent aux parties variables de l'adresse du mailslot. L'adresse complète peut être connue en consultant la propriété MailslotAddress qui en assemble les différentes parties :
\\propriété Location\mailslot\propriété MailslotName

- Le serveur ne propose pas de propriété Location, car cela n'a pas de sens puisque nous avons vu qu'un processus ne peut créer que des mailslots locaux.

- Les méthodes Connect et Disconnect devront être appelées pour créer/ouvrir le mailslot et pour le refermer. La propriété Connected permet de savoir si le mailslot est ouvert.

- Les méthodes Writexxx et Readxxx permettent d'utiliser le mailslot avec un buffer, un flux de données (TStream) ou une simple chaîne de caractères.

V - Liens

- Voici un lien vers la description des mailslots disponible sur le site Microsoft Developer Network (MSDN) : Platform SDK: Interprocess Communications : Mailslots

- Le projet de démonstration incluant l'unité mailslots.pas (Delphi 7) : DelphiMailslots.zip