Création d'un système de chat en Pascal

Mick605

Cet article a pour but de vous apprendre à créer un système de chat entièrement en Pascal. Il fait suite au
Défi Pascal 2010 : Création d'un système de chat.

Commentez

- Titre : Création d'un système de chat en Pascal

- Auteur : Mick605

- Parution : 31 juillet 2011

- Mise à jour : 24 août 2011

- 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 ® 2011 Mick605. 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

- Introduction

- 1. Présentation

- 1-A. Analyse du problème

- 1-B. Architecture

- 1-C. Outils et composants

- 2. Fonctionnement des composants TCP Indy

- 3. Mise en place du chat

- 3-A. Serveur

- 3-B. Client

- 4. Tests et utilisation

- 5. Amélioration du système et création d'un protocole de communication

- 5-A. Le protocole de communication

- 5-B. Connexion d'un client

- 5-C. Analyse et décomposition des messages

- 5-C-1. Côté client

- 5-C-2. Côté serveur

- 5-C-3. Codes sources

- Conclusion

Introduction

Dans ce tutoriel, vous apprendrez à créer un système de chat en Pascal, permettant à plusieurs personnes de communiquer entre elles. Pour ce tutoriel, les bases de la Programmation Orientée Objet (POO) ainsi que la connaissance des threads sont nécessaires.

1 - Présentation

1-A - Analyse du problème

Le but recherché est de pouvoir échanger du texte entre plusieurs ordinateurs. Pour cela, nous avons le choix entre deux protocoles de communication principaux, le TCP et l'UDP.

UDP pour User Datagram Protocol est un protocole de communication simple : il ne nécessite pas de procédure de connexion avant l'envoi des données, on dit qu'il fonctionne en mode "non connecté". Son principal atout est la rapidité de transmission, mais il ne garantit pas la réception du paquet.

TCP pour Transmission Control Protocol est un protocole de communication fiable : il fonctionne en mode connecté, c'est-à-dire que l'envoi de données doit être précédé d'une phase de connexion avec l'ordinateur distant. Ce protocole garantit donc la réception du message, mais il est bien plus lent que UDP. C'est celui que nous allons utiliser.

1-B - Architecture

L'architecture du chat peut prendre deux formes :

- un ordinateur joue le rôle de serveur, il reçoit et envoie tous les messages aux différents clients ;

- chaque ordinateur est à la fois serveur et client, c'est-à-dire que chaque ordinateur s'occupe d'envoyer à tous les clients chaque message.

Par souci de simplicité, nous utiliserons la première configuration.

1-C - Outils et composants

Nous allons maintenant choisir un composant qui gère le protocole TCP. J'ai choisi la bibliothèque de composants Indy 10, qui propose deux composants simples à utiliser, IdTCPServer et IdTCPClient.

2 - Fonctionnement des composants TCP Indy

Du côté serveur comme du côté client, il est nécessaire d'utiliser un thread qui aura pour rôle de vérifier régulièrement la réception des messages.

Le composant IdTCPServer crée automatiquement un thread (un IdContext) propre à chaque client lors de sa connexion. Ce thread appelle l'évènement OnConnect, puis l'évènement OnExecute en boucle. Cet évènement OnExecute nous permettra de vérifier régulièrement la réception d'un message. Lors de la déconnexion, l'évènement OnDisconnect est appelé. Pour communiquer avec un client particulier, il suffit de récupérer son IdContext dans la liste des IdContext du composant.

Le composant IdTCPClient ne crée pas de thread, il sera donc nécessaire de le faire nous-mêmes. Pour commencer l'échange, il faut lancer la procédure de connexion, et si aucune exception n'est levée, nous pourrons continuer l'échange. Notre thread aura pour rôle d'appeler en boucle la méthode servant à la lecture des messages.

Comme les évènements sont appelés à partir du thread, il faut, si nécessaire, utiliser les méthodes de synchronisation pour accéder aux objets ou aux éléments de la VCL (sous Delphi) ou LCL (sous Lazarus).

3 - Mise en place du chat

Pour faire communiquer les composants, nous utiliserons les méthodes ReadLn et WriteLn de la classe IOHandler, donc les messages seront de simples chaines de caractères.

3-A - Serveur

Nous allons créer un serveur basique. À la réception d'un message, le serveur le renverra à tous les clients. Nous allons donc créer une fiche avec seulement un composant IdTCPServer dessus.

Mettre la propriété Active du composant à true, et DefaultPort à 3333 (par exemple)

Les évènements OnConnection et OnExecute

procedure TFServeur.IdTCPServer1Connect(AContext: TIdContext);
begin
//Envoi d'un message au client qui vient de se connecter
AContext.Connection.IOHandler.WriteLn(' --- Bienvenue sur le serveur --- ');
end;

procedure TFServeur.IdTCPServer1Execute(AContext: TIdContext);
var Recept : string;
ListeContext : TList;
i : integer;
begin
//Dès qu'un message est disponible, nous enregistrons l'IP du client et le message dans la variable
Recept:=AContext.Binding.PeerIP+' : '+AContext.Connection.IOHandler.ReadLn;

//Nous récupérons la liste des IdContext qui correspond à la liste des clients
ListeContext:=IdTCPServer1.Contexts.LockList;

//Pour chaque client, on envoie le message précédemment stocké dans la variable Recept
for i:=0 to ListeContext.Count-1 do
TIdContext(ListeContext.Items[i]).Connection.IOHandler.WriteLn(Recept);

//Comme nous avons appelé LockList, nous devons débloquer la liste.
IdTCPServer1.Contexts.UnlockList;

//À partir d'ici, le composant va rappeler l'évènement OnExecute, après avoir vérifié que la connexion est toujours active
end;

3-B - Client

Pour le client, nous avons besoin d'une fiche un peu plus complète :

Exemple d'interface pour le client

Le champ Hôte permettra la saisie de l'adresse IP de l'ordinateur hôte. Notre fiche s'appelle FClient, et les composants sont :

Liste des composants

IdTCPClient1: TidTCPClient;         //Serveur TCP
EMessage: TEdit;                    //Zone de saisie des messages
BConnect: TButton;                  //Bouton de connexion
BDisconnect: TButton;               //Bouton de déconnexion
BSend: TButton;                     //Bouton d'envoi des messages
MChat: TMemo;                       //Zone d'affichage des messages
EHost: TLabeledEdit;                //Zone de saisie de l'adresse IP hôte

Comme nous avons dit plus haut, il nous faut créer un thread qui s'occupera de lire les nouveaux messages. Voici sa définition :

Définition du thread de réception

type
//Le type de l'évènement OnReception
TReceptEvent = procedure (Sender : TObject; Recept : string) of object;

TReceptionthread = class(TThread)
private
FRecept : string;
FIdClient : TIdTCPClient;
FOnReception : TReceptEvent;
protected
procedure Execute; override;
procedure DoOnReception;
public
constructor Create (CreateSuspended : boolean; AIdTCPClient : TIdTCPClient);

//L'évènement OnReception
property OnReception : TReceptEvent read FOnReception write FOnReception;
end;

Le code associé à cet objet est :

Code du thread de réception

constructor TReceptionThread.Create (CreateSuspended : boolean; AIdTCPClient : TIdTCPClient);
begin
inherited Create(CreateSuspended);
FIdClient:=AIdTCPClient;
end;

procedure TReceptionThread.DoOnReception;
begin
//Si l'évènement OnReception a été assigné, alors on l'exécute en lui fournissant les bons paramètres
if Assigned(FOnReception) then
FOnReception(Self,FRecept);
end;

procedure TReceptionThread.Execute;
begin
//On exécute en boucle jusqu'à ce que le thread se termine
while not Terminated do
begin
FRecept:='';

//On utilise une structure Try Except qui permet de détecter la déconnexion du client.
//Si le client se déconnecte, ReadLn provoque une exception, et donc le thread se terminera.
try
FRecept:=FIdClient.IOHandler.ReadLn;
except
Terminate;
end;

if FRecept<>'' then
//On synchronise l'évènement OnReception pour pouvoir utiliser les éléments de la VCL
Synchronize(DoOnReception);
end;
end;

Nous allons maintenant coder la fiche pour qu'elle réponde à nos attentes : après avoir cliqué sur Connecter, l'utilisateur pourra taper du texte dans le champ associé et l'envoyer grâce au bouton Envoyer. Voici le code associé à la fiche, n'oubliez pas d'ajouter les définitions suivantes dans l'objet FClient.

Définition dans l'objet FClient

FReceptionThread : TReceptionThread;
procedure ReceptionMessage (Sender : TObject; Recept : string);

Code de la fiche FClient

procedure TFClient.BConnectClick(Sender: TObject);
begin
//Nous vérifions si l'adresse IP saisie est valide, dans le cas contraire, nous en informons l'utilisateur
if not GStack.IsIP(EHost.Text) then
ShowMessage('L''adresse IP saisie est invalide.')
else
begin
try
//Nous nous connectons. Si cette ligne déclenche une exception, la suite ne sera pas exécutée.
//Le port saisi ici doit être le même que celui du serveur.
IdTCPClient1.Connect(EHost.Text,3333);

//Nous créons notre thread destiné à gérer la réception des messages.
FReceptionThread:=TReceptionthread.Create(false,IdTCPClient1);
// Nous lui affectons aussi son évènement OnReception.
FReceptionThread.OnReception:=ReceptionMessage;

//Nous activons les composants de notre fiche
BDisconnect.Enabled:=true;
BConnect.Enabled:=false;
BSend.Enabled:=true;
EMessage.Enabled:=true;
MChat.Enabled:=true;
except
ShowMessage('Connexion avec le serveur impossible.');
end;
end;
end;

procedure TFClient.BDisconnectClick(Sender: TObject);
begin
//procédure de déconnexion
IdTCPClient1.Disconnect;

//Désactivation des composants de la fiche
BDisconnect.Enabled:=false;
BConnect.Enabled:=true;
BSend.Enabled:=false;
EMessage.Enabled:=false;
MChat.Enabled:=false;
end;

procedure TFClient.BSendClick(Sender: TObject);
begin
//Envoi du texte de l'édit
IdTCPClient1.IOHandler.WriteLn(EMessage.Text);
EMessage.Clear;
end;

procedure TFClient.ReceptionMessage (Sender : TObject; Recept : string);
begin
//Évènement OnReception de notre objet TReceptionThread
//Si nous n'avions pas utilisé la méthode Synchronize, l'utilisation du Mémo Mchat pourrait poser des problèmes.

MChat.Lines.Add(Recept);
end;

Nous venons de créer un chat basique qui permet d'échanger du texte entre plusieurs ordinateurs.

Télécharger les sources du Système de Chat v1 (Miroir)

4 - Tests et utilisation

Il est maintenant temps de tester notre système. Pour cela, il ne nous est pas nécessaire d'avoir deux ordinateurs connectés en réseau : un seul suffit. Il vous suffit de choisir comme adresse hôte l'adresse « 127.0.0.1 ». C'est une adresse de bouclage (localhost) qui correspond à l'ordinateur local. Elle permet de simuler un réseau sur un seul ordinateur.

Pour déboguer le serveur, il vous suffit de l'ouvrir avec votre compilateur, et d'exécuter en même temps le client. De même, si vous voulez déboguer le client, ouvrez-le dans votre compilateur et exécutez le serveur à part.

Pour tester votre système sur un réseau local, il vous suffit d'exécuter un serveur sur un ordinateur, et d'exécuter les clients sur tous les autres ordinateurs. L'adresse IP hôte à saisir devra être celle du serveur.

Sous Windows, pour obtenir l'adresse IP de votre ordinateur, ouvrez une invite de commande et saisissez « ipconfig ». Vous n'avez plus qu'à lire l'adresse IPv4 de la carte réseau qui correspond à la connexion utilisée (Réseau sans fil si vous êtes connecté par Wi-fi, Réseau local sinon).
Sous Linux, l'adresse IP s'obtient en saisissant la commande « ifconfig » dans la console.

5 - Amélioration du système et création d'un protocole de communication

Notre système fonctionne, mais il est très limité. Il se contente d'envoyer du texte entre plusieurs utilisateurs. Il serait intéressant, par exemple, de savoir qui est réellement connecté grâce à des pseudos, ainsi qu'une liste des personnes connectées. Nous devrons donc différencier les messages « texte » (envoyés par les utilisateurs, destinés à être affichés) des messages « techniques » (destinés à effectuer une action précise, par exemple, la connexion d'un nouveau client doit ajouter une ligne à la liste des personnes connectées).

Il apparaît un problème : nous devons être capables d'envoyer dans un seul et même message plusieurs informations. C'est pour cela que nous allons créer un protocole de communication qui nous permettra de gérer tout cela facilement.

5-A - Le protocole de communication

Nous allons donc définir un protocole de communication personnel, qui nous permettra d'envoyer plus d'informations qu'un simple texte. Autrement dit, nous allons nous imposer une structure pour les chaines de caractères, qui nous permettra de stocker plusieurs informations à la fois, mais aussi de différencier les messages purement techniques d'avec les messages « texte ». De plus, pour éviter d'envoyer dans chaque message le pseudonyme de l'émetteur, nous allons leur attribuer un identifiant unique (ID) lors de leur connexion. Cet identifiant sera simplement un entier, qui sera envoyé sur trois caractères dans les messages.

Voici un exemple de protocole que nous pouvons utiliser :

Types de messages du serveur vers le client

Repère

Structure

Signification

Exemple

*

* + ID + Contenu

Message « texte »

'*001Salut'

O

O + ID

Connexion établie

'O001'

N

N + Raison

Connexion échouée

'NPseudo déjà utilisé'

C

C + ID + Pseudo

Annonce de connexion d'un membre

'C001Mick605'

D

D + ID

Annonce de déconnexion d'un membre

'D001'

Types de messages du client vers le serveur

Repère

Structure

Signification

Exemple

*

* + ID + Contenu

Message « texte »

'*001Salut'

C

C + Pseudo

Demande de connexion au serveur

'CMick605'

Ainsi, chacune de ces structures de message sera analysée de manière différente, côté client aussi bien que côté serveur. Le premier caractère d'un message nous permet directement de différencier la procédure à suivre. Le découpage des messages et la récupération des informations seront simples, car nous savons que les ID sont composés de trois caractères.

5-B - Connexion d'un client

La connexion d'un client est un peu spéciale : le serveur doit vérifier plusieurs paramètres comme par exemple, si une place est disponible, ou que le pseudonyme n'est pas déjà utilisé par un autre client. De plus, le serveur doit fournir au client son identifiant ainsi que la liste des personnes connectées. Voici un schéma de la procédure de connexion d'un client.

Chaque flèche représente l'envoi d'un message d'un ordinateur à l'autre. Dans chaque cadre jaune est noté un exemple de message pouvant être envoyé, en utilisant le protocole vu en 5.A.

5-C - Analyse et décomposition des messages

Maintenant que nous avons défini notre protocole, il nous faut créer les méthodes permettant de décomposer et d'analyser les chaines de caractères reçues, autant pour le serveur que pour le client.

5-C-1 - Côté client

Nous modifions notre fiche pour y ajouter deux composants : un Edit nommé Epseudo pour saisir le pseudonyme, et une ListBox nommée LBClientsConnectes qui contiendra la liste des clients connectés.

Exemple d'interface pour le client

Nous modifions aussi notre code pour y inclure les variables nécessaires. Nous déclarons un entier nommé MonID destiné à contenir l'ID du client, et un tableau de chaine de caractères nommé Users qui contiendra le pseudonyme de tous les utilisateurs. L'identifiant ID jouera le rôle d'indice pour le tableau, c'est-à-dire que le client dont l'identifiant sera 003 sera stocké dans la case 3 du tableau Users.

Nous modifions le code de connexion :

Procédure de demande de connexion

procedure TFClient.BConnectClick(Sender: TObject);
begin
if EPseudo.Text='' then
ShowMessage('Vous devez saisir un pseudo.')
else
begin
//Nous vérifions si l'adresse IP saisie est valide, dans le cas contraire, nous en informons l'utilisateur
if not GStack.IsIP(EHost.Text) then
ShowMessage('L''adresse IP saisie est invalide.')
else
begin
try
//Nous nous connectons. Si cette ligne déclenche une exception, la suite ne sera pas exécutée.
//Le port saisi ici doit être le même que celui du serveur.
IdTCPClient1.Connect(EHost.Text,3333);

//Envoi du message de demande de connexion. À ce stade, nous ne sommes pas encore en capacité de
//communiquer avec les autres clients. Le serveur doit d'abord vérifier quelques paramètres, et
//nous renvoyer une réponse
IdTCPClient1.IOHandler.WriteLn('C'+EPseudo.Text);

//Nous créons notre thread destiné à gérer la réception des messages.
FReceptionThread:=TReceptionthread.Create(false,IdTCPClient1);
// Nous lui affectons aussi son évènement OnReception.
FReceptionThread.OnReception:=ReceptionMessage;
except
ShowMessage('Connexion avec le serveur impossible.');
end;
end;
end;
end;

Nous devons aussi modifier le code permettant d'envoyer le texte (bouton BSend) pour prendre en compte le protocole que nous avons établi, c'est-à-dire que les messages texte doivent commencer par * suivi de l'ID de l'émetteur :

Procédure d'envoi d'un message Texte

procedure TFClient.BSendClick(Sender: TObject);
begin
//Envoi du texte de l'edit
IdTCPClient1.IOHandler.WriteLn('*'+Format('%3i',[MonID])+EMessage.Text);
EMessage.Clear;
end;

Et nous modifions enfin notre procédure de réception des messages :

Procédure ReceptionMessage

procedure TFClient.ReceptionMessage (Sender : TObject; Recept : string);
//Évènement OnReception de notre objet TReceptionThread

function ExtractIDUser (Mess : string) : integer;
//fonction qui extrait les trois premiers caractères du message et retourne l'ID
begin
if Length(Mess)>=2 then
Result:=StrToIntDef(Mess[1]+Mess[2]+Mess[3],-1)
else
Result:=-1;
end;

procedure ConnectionSucceed (R : string);
//R contient un message de connexion réussie de la forme << ID >> qui signifie que nous sommes connectés avec l'identifiant ID
begin
//MonID est une variable globale contenant notre identifiant
MonID:=ExtractIDUser(R);

//Stockage de notre pseudo dans le tableau d'utilisateurs
Users[MonID]:=EPseudo.Text;

//Nous annonçons notre connexion et nous activons les composants nécessaires
MChat.Lines.Add(' --- Connexion établie --- ');
BDisconnect.Enabled:=true;
BConnect.Enabled:=false;
BSend.Enabled:=true;
EMessage.Enabled:=true;
MChat.Enabled:=true;
EPseudo.Enabled:=false;
LBClientsConnectes.Items.Add('- Liste des clients connectés -');

//Nous ajoutons notre pseudo dans la ListBox
LBClientsConnectes.Items.Add(EPseudo.Text);
end;

procedure ConnectionFailed (R : string);
//R contient un message de connexion échouée de la forme << Raison >> qui signifie que nous ne sommes pas connectés car : Raison
begin
//Nous affichons les informations nécessaires
MChat.Lines.Add(' --- Connexion échouée --- ');
MChat.Lines.Add('Raison : '+R);

//Nous nous déconnectons du serveur
IdTCPClient1.Disconnect;
end;

procedure UserConnection(R : string);
//R contient un message de connexion de la forme << ID + Pseudo >> qui signifie que Pseudo vient de se connecter avec l'identifiant ID
var ID : integer;
Pse : string;
begin
//ID contient l'identifiant du message
ID:=ExtractIDUser(R);
//Pse contient le Pseudo
Pse:=Copy(R,4,MaxInt);
//Nous enregistrons l'utilisateur dans le tableau
Users[ID]:=Pse;

//Nous annonçons la connexion du client
MChat.Lines.Add(Pse+' vient de se connecter.');

//Nous ajoutons son nom dans la ListBox
LBClientsConnectes.Items.Add(Pse);
end;

procedure UserDeconnection(R : string);
//R contient un message de déconnexion de la forme << ID >> qui signifie que la personne avec l'identifiant ID vient de se déconnecter
var ID : integer;
Pse : string;
begin
//ID contient l'identifiant du message
ID:=ExtractIDUser(R);
//Pse contient le Pseudo sauvegardé lors de la connexion
Pse:=Users[ID];

//Nous annonçons la déconnexion du client
MChat.Lines.Add(Pse+' vient de se déconnecter.');

//Nous effaçons du tableau le client déconnecté
Users[ID]:='';

//Nous cherchons et supprimons son nom dans la ListBox
I:=LBClientsConnectes.Items.IndexOf(Pse);
if I<>-1 then LBClientsConnectes.Items.Delete(I);
end;

procedure ReceptMessageTexte (R : string);
//R contient un message texte de la forme << ID + Texte >> qui signifie que la personne avec l'identifiant ID dit "Texte"
var ID : integer;
Pse : string;
Texte : string;
begin
//ID contient l'identifiant du message
ID:=ExtractIDUser(R);
//Pse contient le Pseudo stocké lors de la connexion
Pse:=Users[ID];
//Texte contient le texte envoyé par l'utilisateur
Texte:=Copy(R,4,MaxInt);

//Nous affichons le texte précédé du pseudo de l'émetteur
MChat.Lines.Add(Pse+' dit :');
MChat.Lines.Add(' > '+Texte);
end;

var Repere : char;
begin
//le premier caractère du message permet de différencier la marche à suivre
Repere:=Recept[1];
//Nous l'effaçons de la chaine de caractères
Delete(Recept,1,1);

//Suivant les cas, on exécute une des procédures ci-dessous
case Repere of
'O' : ConnectionSucceed(Recept);
'N' : ConnectionFailed(Recept);
'C' : UserConnection(Recept);
'D' : UserDeconnection(Recept);
'*' : ReceptMessageTexte(Recept);
end;
end;

5-C-2 - Côté serveur

Du côté du serveur, nous avons plusieurs éléments à ajouter. En effet, le serveur doit stocker la liste de tous les utilisateurs et leurs caractéristiques.

Premièrement, nous allons créer un type TUser, qui contiendra les caractéristiques d'un utilisateur (Pseudonyme, Identifiant, Statut de la connexion). Voici sa définition :

Définition des types TUser et TConnexionState

type
TConnexionState = (csOk, csDisconnecting);

TUser = class
private
FPseudo : string;
FID : integer;
FState : TConnexionState;
public
constructor Create (APseudo : string; AID : integer);

property Pseudo : string read FPseudo;
property ID : integer read FID;
property State : TConnexionState read FState write FState;
end;

...

constructor TUser.Create (APseudo : string; AID : integer);
//Nous créons notre objet TUser en l'initialisant avec les bonnes valeurs
begin
inherited Create;

FPseudo:=APseudo;
FID:=AID;
FState:=usOk;
end;

L'objet TIdContext qui, rappelons-le, correspond à une connexion avec un client, possède une propriété Data, de type Pointer, qui permet de pointer sur tout type d'information. Nous allons nous servir de cette propriété pour pointer sur un objet de type TUser. Ainsi, en possédant uniquement l'IdContext, nous pourrons retrouver les caractéristiques de l'utilisateur associé. Nous allons aussi stocker ces objets TUser dans un tableau Users déclaré comme suit :

Users : array [1..MAXUSERS] of TUser;

Nous définissons maintenant une procédure SendAll, qui aura pour but d'envoyer un message à tous les clients connectés.

Procédure SendAll

procedure TFServeur.SendAll (Mess : string);
var ListeContext : TList;
i : integer;
AUser : TUser;
begin
//Nous récupérons la liste des IdContext qui correspond à la liste des clients
ListeContext:=IdTCPServer1.Contexts.LockList;

//Pour chaque client prêt (State = usOK), on envoie le message Mess
for i:=0 to ListeContext.Count-1 do
begin
AUser:=TUser(TIdContext(ListeContext.Items[i]).Data);

if Assigned(AUser) and (AUser.State=usOk) then TIdContext(ListeContext.Items[i]).Connection.IOHandler.WriteLn(Mess);
end;

//Comme nous avons appelé LockList, nous devons débloquer la liste.
IdTCPServer1.Contexts.UnlockList;
end;

Nous codons l'évènement OnDisconnect :

Événement OnDisconnect du IdTCPServer1

procedure TFServeur.IdTCPServer1Disconnect(AContext: TIdContext);
var AUser : TUser;
begin
//On stocke dans une variable temporaire les caractéristiques de l'utilisateur qui se déconnecte
AUser:=TUser(AContext.Data);

if Assigned(AUser) then
begin
//On met son statut en Disconnecting pour éviter qu'il ne reçoive le message lors du SendAll suivant
AUser.State:=csDisconnecting;

//On envoie à tous les clients l'annonce de la déconnexion
SendAll('D'+Format('%.3d',[AUser.ID])+AUser.Pseudo);

//On efface le pointeur Data et on libère le client du tableau Users
AContext.Data:=nil;
Users[AUser.ID].Free;
end;
end;

Et enfin, l'évènement OnExecute :

Évènement OnExecute du IdTCPServer1

procedure TFServeur.IdTCPServer1Execute(AContext: TIdContext);
var Recept : string;
begin
//Lecture du message
Recept:=AContext.Connection.IOHandler.ReadLn;

//Analyse du message
GestionMessage(AContext,Recept);
end;

La procédure GestionMessage permet d'analyser chaque message. Lors d'une demande de connexion, le serveur vérifie la place disponible, et si le pseudonyme est déjà utilisé. Lorsqu'il s'agit d'un message texte, le serveur se contente de le renvoyer à tous sans y effectuer de modifications.

Procédure de Gestion des messages

procedure TFServeur.GestionMessage (AContext : TIdContext; Recept : string);

procedure EnvoiListeUsers;
var i : integer;
begin
//On parcourt le tableau Users et on envoie la liste des personnes connectées à notre client
for i:=1 to MAXUSERS do
if Assigned(Users[i]) and (Users[i].State=csOk) then
AContext.Connection.IOHandler.WriteLn('C'+Format('%.3d',[i])+Users[i].Pseudo);
end;

procedure DemandeConnection(R : string);
//R contient un message de demande de connexion de la forme << Pseudo >> qui signifie que Pseudo veut se connecter
var Raison : string;
IsPossibleConnection : boolean;
ID : integer;
begin
IsPossibleConnection:=true;

//Nous testons si une personne ne possède pas le même pseudo
ID:=0;
repeat
inc(ID);
until (ID>MAXUSERS) or (Assigned(Users[ID]) and (Users[ID].Pseudo=R));

if ID<=MAXUSERS then
begin
IsPossibleConnection:=false;
Raison:='Pseudo déjà utilisé';
end;

if IsPossibleConnection then
begin
//Nous vérifions s'il reste de la place dans le serveur
ID:=1;
while (ID<=MAXUSERS) and Assigned(Users[ID]) do
inc(ID);

if ID>MAXUSERS then
begin
IsPossibleConnection:=false;
Raison:='Serveur plein'
end;
//ici, la variable ID contient l'indice de la première place libre dans le tableau Users
end;

//Maintenant que nos tests sont finis, nous répondons à l'utilisateur
if not IsPossibleConnection then
begin
//Connexion impossible
AContext.Connection.IOHandler.WriteLn('N'+Raison);
end
else
begin
//Connexion possible avec l'identifiant ID
AContext.Connection.IOHandler.WriteLn('O'+Format('%.3d',[ID]));

//Envoi de la liste des personnes déjà connectées
EnvoiListeUsers;

//Annonce de la connexion de l'utilisateur à tous les autres utilisateurs
SendAll('C'+Format('%.3d',[ID])+R);

//Stockage dans le tableau Users
Users[ID]:=TUser.Create(R,ID);

//On fait pointer la propriété Data du IdContext vers notre nouveau TUser
AContext.Data:=Users[ID];
end;

end;

procedure ReceptMessageTexte (R : string);
//R contient un message texte de la forme << ID + Texte >> qui signifie que la personne avec l'identifiant ID dit "Texte"
begin
SendAll('*'+R);
end;

var Repere : char;
begin
//Le premier caractère du message permet de différencier la marche à suivre
Repere:=Recept[1];
//Nous l'effaçons de la chaine de caractères
Delete(Recept,1,1);

//Suivant les cas, on exécute une des procédures ci-dessous
case Repere of
'C' : DemandeConnection(Recept);
'*' : ReceptMessageTexte(Recept);
end;
end;

5-C-3 - Codes sources

Télécharger les sources du Système de Chat v2 (Miroir)

Conclusion

Nous possédons maintenant un système de chat fonctionnel, permettant de voir les utilisateurs connectés grâce à l'utilisation de pseudonymes. Ce système de chat peut servir de base pour prendre en charge plus de fonctionnalités, comme l'ajout de statuts, la personnalisation du texte, etc. Ma participation au Défi Pascal 2010 est basée sur ce système.

Je tiens à remercier l'équipe Pascal, et tout particulièrement Alcatîz, Krachik et Darrylsite pour l'aide apportée à la rédaction et à la correction de l'article, ainsi que Claude Leloup pour la relecture orthographique.