Un Chat

Un chat en C#

 

Version PDF

Un chat utilise un mécanisme de communication entre deux applications distantes. Il existe plusieurs solutions pour faire communiquer deux applications ; nous allons, ici, utiliser le protocole UDP. Ce protocole (couche transport) est utilisé sur internet pour des échanges en temps réel, peu sensibles, ne nécessitant pas le maintien de la connexion.

Le protocole est basé sur la notion de socket.

Extrait de Wikipédia

Dotnet propose plusieurs classes pour mettre en oeuvre cette communication, de plus ou moins haut niveau (encapsulant plus ou moins de services). La classe de plus haut niveau est la classe UdpClient. Elle fournit des services de connexion, emission et réception de messages.

La classe UdpClient

L'utilisation de cette classe nécessite la déclaration deux namespaces :
using System.Net.Sockets;
using System.Net;

Cette classe dispose de plusieurs constructeurs surchargés, dont certains peuvent réaliser une connexion (par défaut) à un hôte distant ainsi que différentes méthodes dont :

- Connect, pour se connecter si ce n'est pas fait par le consructeur
- Send, pour envoyer des données (sous la forme d'un tableau d'octets)
- Receive, pour recevoir des données

Ceci est suffisant pour envoyer un message d'une application cliente vers une application serveur

La classe IPEndPoint

Cette classe décrit un point de connexion ; couple IP/port. Elle est souvent utilisée par un objet UdpClient pour signifier le destinataire des connexions et/ou des messages.

1) Une application simpliste de message client/serveur.

Il s'agit simplement, à partir d'une application client, d'envoyer un message sur une application serveur :

         1.1 L'application cliente

Le code (ici dans l'événement click) est réduit :

/* Ligne 1 */        UdpClient udp = new UdpClient();
/* Ligne 2 */        udp.Connect(txtDestinataire.Text, 1500);
/* Ligne 3 */        message = Encoding.Unicode.GetBytes(txtMessage.Text);
/* Ligne 4 */        udp.Send(message, message.Length);
/* Ligne 5 */        udp.Close();

Commentaires
Ligne 1 : création d'un objet. On pouvait également ne pas se connecter explicitement en fournissant les arguments de Connect dans la construction.
Ligne 2 : on peut utiliser l'adresse IP ou (ici) le nom DNS et le port d'écoute (1500).
Ligne 3 : le message doit être converti en tableau d'octets
Ligne 4 : tout pouvait se faire ici !! udp.Send(message, message.Length,txtDestinataire.Text, 1500);

         1.2 L'application serveur

C'est elle qui "écoute", le code (ici dans l'événement click) est aussi réduit:

/* Ligne 1 */       UdpClient udp = new UdpClient(1500);
/* Ligne 2 */       IPEndPoint EmetteurIpEndPoint = new IPEndPoint(IPAddress.Any, 1500);
/* Ligne 3 */       Byte[] donneesRecues = udp.Receive(ref EmetteurIpEndPoint);
/* Ligne 4 */       string message = Encoding.Unicode.GetString(donneesRecues);
/* Ligne 5 */       string nom = EmetteurIpEndPoint.Address.ToString();
/* Ligne 6 */       txtMessageRecu.Text=("message de " + nom + " : " + message );
/* Ligne 7 */      udp.Close();

Commentaires
Ligne 1 : la définition du port par défaut est nécéssaire ici
Ligne 2 : création d'un point terminal de connexion ; toutes les connexions (IPAddress.Any) sur le port 1500 sont écoutées.
Ligne 3 : récupération des données reçues (dans un tableau de caractères) grâce à la méthode Receive. L'argument de type IPEndPoint est renseigné à ce moment -noter le passage par ref-. C'est lui qu'il faudra interroger pour avoir les informations sur l'émetteur (ligne 5 )
Ligne 4 : conversion du tableau de caractères en string
Ligne 5 : interrogation du IPEndPoint pour obtenir l'adresse de l'émetteur

Travail à faire.
Développer les deux applications. Tester
Questions : Comment réagit l'application serveur lorsqu'elle attend un message d'un client ? que se passe t-il pour l'application serveur si on envoie plusieurs messages d'un client? Expliquez pourquoi.

         1.3 Evolution : un serveur et plusieurs clients

Dans le scénario précédent l'application serveur est bloquée dans l'attente d'un message ; par aillleurs après la réception d'un message il faut reconnecter le serveur pour recevoir un nouveau message. Le fonctionnement est en mode synchrone (bloquant dans l'attente d'un message). Il est parfois nécessaire de mettre en oeuvre des mécanismes asynchrones afin de simuler des fonctionnement parallèles. En développement ce mécanisme est basé sur une programmation multithread. Un thread est une mini-tâche d'une application ; la programmation multithread consiste à simuler des mini-tâches parallèles afin qu'aucune ne soit bloquante pour les autres. La plupart des langages fournissent des ressources (classes ou méthodes) permettant de mettre en oeuvre ce mécanisme.

Dotnet expose des méthodes asynchrones ; elles sont préfixées Begin et End de l'équivalent des méthodes synchrones. Ainsi, Receive permettait de récupérer les données en mode synchrone : BeginReceive et EndReceive le feront en mode asynchrone.

Mise en oeuvre.
Ajouter une zone de liste dans l'application serveur afin de récupérer les messages.

L'application cliente est inchangée

Dans l'application serveur :
           1.3.a Dans l'événement click.

/* Ligne 1 */  UdpClient udp = new UdpClient(1500);
/* Ligne 2 */  AsyncCallback appelAsynchrone = new AsyncCallback(receptionMessage);
/* Ligne 3 */  udp.BeginReceive(appelAsynchrone, udp);

Commentaires :
Ligne 1 : création d'un objet d'écoute
Ligne 2 : création d'un objet pour l'appel asynchrone. L'argument du constructeur est une méthode : celle qui sera appelée à chaque réception de message
Ligne 3 : appel de la méthode asynchrone de réception. Le deuxième argument est de type Objet et laissé à la liberté du développeur ; il sera utilisé comme argument dans la méthode traiteMessage.

             1.3.b la méthode receptionMessage

        private void receptionMessage(IAsyncResult ar)
        {
 /* Ligne 1 */               UdpClient e = (UdpClient)(ar.AsyncState);
/* Ligne 2 */               IPEndPoint EmetteurIpEndPoint = new IPEndPoint(IPAddress.Any, 1500);
/* Ligne 3 */                Byte[] tabBytes = e.EndReceive(ar, ref EmetteurIpEndPoint);
/* Ligne 4 */                string message = Encoding.Unicode.GetString(tabBytes);
/* Ligne 5 */               lstMessages.Items.Add(message);
/* Ligne 6 */                AsyncCallback appelAsynchrone = new AsyncCallback(traiteMessage);
/* Ligne 7 */                e.BeginReceive(appelAsynchrone, e);
         }

Ligne 1 : récupération de l'objet d'écoute de type UdpClient (cf ligne 3 plus haut)
Ligne 3 : appel d'une méthode de fin de réception qui récupère les données et valorise le point de connexion (comme pour la méthode synchrone)
Lignes 6 et 7 appel réentrant de la méthode traiteMessage à chaque nouvelle réception.

Travail à faire.
Modifier l'application en conséquence. Dans un premier temps tester en lançant sans débogage (CTRL + F5) ; envoyez plusieurs messages à partir d'un ou plusieurs clients.
Dans un second temps tester l'application en mode débogage (F5) ; que constatez-vous. Suivez le conseil de débogage, effectuez la modification évoquée dans la partie Remarque.

2) Une application un peu plus consistante.

Nous sommes prêts maintenant à développer une application plus réaliste. Une application qui va permettre de faire converser plusieurs utilisateurs à partir d'un serveur unique.

             2.1 Analyse

                       2.1.a 3 cas d'utilisation :

Cas d'utilisation connecter :
1. L'utilisateur fournit l'IP du serveur de chat et demande la connexion
2. Le système -le serveur- enregistre ce nouvel inscrit

Cas d'utilisation envoyer un message :
1. L'utilisateur rédige et envoie un message
2. Le système -le serveur- reçoit le message et le retourne à tous les incrits en indiquant le nom de l'émetteur

Cas d'utilisation déconnecter :
1. L'utilisateur demande à se déconnecter ou ferme son appliction
2. Le système -le serveur- retire cet inscrit à sa liste

Deux applicationt sont nécessaires :

                      2.1.b L'application serveur
Son interface est des plus sobres !

Responsabilités :
- Enregistrer un nouvel insrit
- Dispacher chaque message reçu vers tous les inscrits
- Supprimer un inscrit, à sa demande

                       2.1.c L'application cliente
L'interface n'est guère plus riche

Responsabilités :
- Envoyer son userName au serveur à la connexion
- Envoyer un message au serveur
- Informer le serveur de sa déconnexion

                     2.2 Mise en oeuvre

                          2.2.a Gestion des messages.

Les deux applications vont communiquer en s'échangeant des messages, de 3 natures différentes ; un message de connexion (sans contenu), un message avec un texte (le chat) et un message de déconnexion (sans contenu). La technique choisie ici est de créer une classe Message ainsi qu'un mécanisme de sérialisation/désérialisation.

La classe Message contient 3 champs privés :
- un champ emetteur (string) qui contiendra le userName
- un champ texte (string) correspondant au contenu envoyé
- un champ action (char ) qui précise le type d'action : connexion ('c'), deconnexion('d'), message ('m').

Deux constructeurs :
- un avec deux arguments, emetteur et l'action
- un autre avec deux arguments, emetteur et texte.

Les 3 accesseurs sur les champs privés.

Travail à faire.
Créer un nouveau projet : le client. Créer un formulaire correspondant à celui présenté plus haut.
Ajouter un nouveau formulaire (test) de test qui ne contiendra qu'un bouton et une zone de texte et qui servira pour les tests.

Modifier dans le fichier Program.cs le formulaire lancé :
Application.Run(new test());
Tester.
Ajouter la nouvelle classe Message au projet ; tester dans le formulaire test.

Le mécanisme de sérialisation.

Une classe SerializeMessage se chargera, grâce à deux méthodes statiques de transformer un tableau de bytes en objet Message et une autre qui fait le travail inverse. On vous fournit une partie du code.

class SerializeMessage
{
         public static Byte[] toBytes(Message m)
        { /* code à écrire*/

         }  
         public static Message getMessage(byte[] bytes)
         {
              MemoryStream flux = new MemoryStream(bytes);
              BinaryFormatter bf = new BinaryFormatter();
               bf.Binder = new DeserializeBinder();
               object o = bf.Deserialize(flux);
               flux.Close();
                return (Message)o;
            }
public class DeserializeBinder : SerializationBinder
{
            public override Type BindToType(string assemblyName,string typeName)
             {
                      return typeof(Message);
              }
}

Commentaire
La classe DeserializeBinder est nécessaire pour la situation où l'application qui sérialise n'est pas celle qui désérialise. Elle n'est pas à utiliser dans le code que vous avez à écrire.

Travail à faire.
Ajouter une classe au projet : SerializeMessage. Remplcer le code généré par celui fourni juste au dessus. Ecrire le code de la méthode toBytes. Tester avec le code suivant :
private void button1_Click(object sender, EventArgs e)
{
         Message m = new Message("toto", "bonjour");
          Byte[] bytes = SerializeMessage.toBytes(m);
         Message m1;
          m1 = SerializeMessage.getMessage(bytes);
        string s = "emetteur : " + m1.getEmetteur() + " action : " + m1.getAction().ToString() + " texte : " + m1.getTexte();
        MessageBox.Show(s);
}

2.2.b L'application cliente

Des attributs privés peuvent être déclarés :
private IPAddress IPServeur;
private int portEmission = 1500;
private int portReception = 1501;
private UdpClient udpReception;
private UdpClient udpEmission;

Trois points d'intervention :
a) Sur l'événement click de connexion
- Construction d'un objet IPAdress à partir de l'adresse IP fournie
- Récupération du userName de l'utilisateur ; demander le service au DNS
- Création d'un objet UdpClient et connexion à partir de lIPAdress du serveur
- Création et envoie au serveur d'un message de connexion
- Mise en oeuvre du processus d'écoute vue dans la première partie ; utiliser un autre objet UdpClient et une autre valeur du port

b) Une méthode privée d'envoie de message
private void envoyer(Message m)

c) La méthode d'écoute
private void receptionMessage(IAsyncResult ar)
qui va charger les messages (sérialisés) reçus dans la liste déroulante

Attention à la gestion des objets de connexion udpEmission et udpReception a utiliser
La deconnexion peut être gérée sur l'évement click de connexion également qui passe à l'état déconnexion après une connexion. Elle peut être en plus faite à la fermeture du formulaire.

Travail à faire.
Développer le formulaire
               

                   2.2.c L'application serveur

Des attributs privés peuvent être déclarés :
private int portEmission = 1501;
private int portReception = 1500;
private UdpClient udpReception;
private UdpClient udpEmission;
private ArrayList lesConnectes;

Trois points d'intervention :

a) Sur l'événement click de connexion
- Création du UdpClient de réception
- Création de l'ArrayList
- Mise en oeuvre du processus d'écoute vue dans la première partie

b) La méthode d'écoute
private void receptionMessage(IAsyncResult ar)
c) Elle appellera une méthode privée :
private void traiteMessage(Byte[] bytes)
qui récupèrera le message (sérialisation), et selon la valeur du champ action :
- cas 'c' : ajout dans la liste des inscrits
- cas 'd' : suppression de la liste des inscrits
- cas 'm' : envoie du message à tous les inscrits

Travail à faire.
Développer le formulaire