Tchatcher avec WCF

Florian Casabianca

Dans cet article nous allons construire pas à pas une petite application permettant à plusieurs personnes de tchatcher ensemble. Cela va nous permettre d'aborder plusieurs notions de WCF : contrats, contrat de rappel, communication duplex, session…

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Tchatcher avec WCF

- Auteur : Florian Casabianca

- Parution : 5 mars 2008

- Mise à jour : 12 mars 2008

- 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 ® 2008 Florian Casabianca. 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. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- Introduction

- I. Présentation de l'application

- II. Les contrats WCF

- II-A. Le contrat de service

- II-B. Le contrat de rappel

- III. Le binding (liaison)

- III-A. Communication unidirectionnelle et duplex

- III-B. Choix du binding

- IV. Implémentation du service

- IV-A. Instanciation et session

- IV-A-1. Connexion d'un client

- IV-A-2. Envoi d'un message

- IV-A-3. Fermeture de session

- IV-B. Concurrence

- V. L'hébergement du service

- V-A. Présentation

- V-B. Zoom sur le code

- VI. Le client WinForm

- VI-A. Présentation

- VI-B. Référence au service

- VI-C. Appel au service

- VI-C-1. Connexion du client

- VI-C-2. Déconnexion du client

- VI-C-3. Envoi d'un message

- VI-D. Implémentation du contrat de rappel

- VI-D-1. Connexion d'un autre utilisateur

- VI-D-2. Déconnexion d'un autre utilisateur

- VI-D-3. Réception d'un message

- VI-D-4. Exemple en image

- Conclusion

- Liens

- Sources

- Remerciements

- Contact

Introduction

Windows Communication Foundation (WCF), disponible depuis le Framework 3.0, est le modèle de programmation unifié de Microsoft permettant de générer des applications orientées service. Vous pourrez trouver à la fin de cet article des liens vers deux articles écrits par Vincent Lainé et Ronald VASSEUR qui présentent WCF. Il vous est conseillé de les lire, car ils abordent des points fondamentaux sur lesquels nous ne reviendrons pas.

Dans cet article nous allons construire pas à pas une petite application permettant à plusieurs personnes de tchatcher ensemble. Cela va nous permettre d'aborder plusieurs notions de WCF : contrats, contrat de rappel, communication duplex, session.

I - Présentation de l'application

Notre application sera composée de deux parties : le service WCF et une application cliente Winform. Le service devra permettre aux clients de démarrer une session, envoyer des messages aux autres utilisateurs connectés et fermer une session. Il devra aussi être en mesure de notifier tous les clients connectés de l'arrivée d'un nouveau message, de la connexion d'un nouvel utilisateur ou encore de la déconnexion d'un utilisateur sans que les clients aient besoin de demander explicitement ce genre d'information.

II - Les contrats WCF

II-A - Le contrat de service

Les contrats de service WCF permettent de définir les services offerts par notre application côté serveur (opérations disponibles, types de données utilisées, etc.).

Voici le contrat de service que nous allons utiliser :

Interface du contrat de service

namespace ChatContrats
{
[ServiceContract(Namespace = "http://www.developpez.com/chatWCF/2008/01",
CallbackContract = typeof(IChatWCFRappel),
SessionMode = SessionMode.Required)]
public interface IChatWCF
{
[OperationContract(IsInitiating = true, IsTerminating = false, IsOneWay = true)]
void DemarrerSession(String pseudo);

[OperationContract(IsInitiating = false, IsTerminating = true, IsOneWay = true)]
void FermerSession();

[OperationContract(IsInitiating = false, IsTerminating = false, IsOneWay = true)]
void EnvoyerMessage(String message);
}

Pour qu'une interface devienne un contrat de service, il suffit de lui appliquer l'attribut ServiceContract. Nous pouvons ensuite spécifier certains attributs qui vont influencer le comportement de ce contrat. Namespace indique l'espace de nommage cible pour les messages; vous pouvez y mettre ce que vous voulez, même s'il est d'usage d'utiliser un nom de société, d'application, etc. CallbackContract permet d'associer un contrat de rappel pour des communications bidirectionnelles. Ce contrat de rappel est lui aussi un contrat de service; nous en parlerons plus en détail dans la partie suivante. Enfin, SessionMode détermine si les sessions doivent être prises en compte. Il existe trois valeurs différentes pour cet attribut : Allowed (valeur par défaut), NotAllowed et Required. La première indique que les sessions seront utilisées si l'implémentation du service et son binding les supportent. La deuxième désactive le support des sessions (et interdit donc l'utilisation de binding les utilisant). Enfin, la troisième oblige le service à supporter les sessions.

L'attribut ServiceContract possède d'autres propriétés que nous laisserons de côté dans notre exemple.

Notre interface est assez simple et ne comporte que trois méthodes dont les noms suffisent à expliquer leur rôle. Ces méthodes, bien que faisant partie de l'interface, ne font pas implicitement partie du contrat de service. Pour cela, il est nécessaire leur appliquer l'attribut OperationContract. Ce dernier possède un certain nombre de propriétés permettant de le personnaliser. Dans notre exemple nous n'en utiliserons que trois.

IsInitiating et IsTerminating permettent d'influencer la création et la destruction des sessions. Une session est par défaut créée lors du premier appel du client au service (sur n'importe quelle méthode). Elle se termine à l'expiration d'un timeout ou par une demande explicite (fermeture du canal de communication par exemple). Il est cependant possible de modifier ce comportement.

Positionnée à true, la valeur de IsInitiating indique qu'un appel à la méthode correspondante créera une session (sauf s’il en existe déjà une). Positionnée à false, cela indiquera que la méthode ne pourra être appelée que si une session existe déjà (dans le cas contraire, une exception sera déclenchée). Ainsi, dans notre contrat, si le premier appel d'un client au service s'effectue sur la méthode EnvoyerMessage, une exception sera déclenchée, car aucune session n'aura été créée. Un client voulant utiliser notre service devra donc obligatoirement appeler la méthode DemarrerSession en premier.

Une valeur true pour la propriété IsTerminating indique que la méthode correspondante terminera la session en cours (si elle existe). Ainsi, dans notre exemple, un client devra appeler la méthode FermerSession lorsqu'il voudra se déconnecter.

Enfin, la propriété IsOneWay va jouer un rôle important dans le fonctionnement de notre service. Sa valeur est par défaut positionnée à false. Lorsqu'elle est positionnée à true cela indique que la méthode correspondante sera unidirectionnelle (au lieu d'être requête-réponse). Nous détaillerons cette notion dans la partie consacrée au binding.

Vous avez surement remarqué que la méthode EnvoyerMessage ne prenait pas en paramètre l'expéditeur du message. Cela n'est pas utile dans la mesure où, grâce aux sessions côté serveur, nous serons capables de déterminer l'identité de l'appelant (nous verrons tout ceci un peu plus bas dans l'implémentation du contrat).

II-B - Le contrat de rappel

Passons maintenant au contrat de rappel. Celui-ci est aussi un contrat de service que devra implémenter le client afin de pouvoir être rappelé par le serveur. Il est à noter que l'attribut ServiceContract n'est ici pas obligatoire; le contrat de rappel hérite des paramètres (concernant ServiceContract) du contrat de service auquel il est associé.

Voici le code du contrat de rappel que nous allons utiliser :

namespace ChatContrats
{
/// <summary>
/// Contrat de rappel que le client devra implémenter pour être rappelé par le serveur
/// </summary>
public interface IChatWCFRappel
{

/// <summary>
/// Informe le client qu'un nouvel utilisateur s'est connecté
/// </summary>
/// <param name="pseudo">pseudo de l'utilisateur qui s'est connecté</param>
[OperationContract(IsOneWay = true)]
void ConnexionUtilisateur(String pseudo);

/// <summary>
/// Informe le client qu'un utilisateur s'est déconnecté
/// </summary>
/// <param name="pseudo">pseudo de l'utilisateur qui s'est déconnecté</param>
[OperationContract(IsOneWay = true)]
void DeconnexionUtilisateur(String pseudo);

/// <summary>
/// Reception par le client d'un message
/// </summary>
/// <param name="message">le message reçu</param>
[OperationContract(IsOneWay = true)]
void ReceptionMessage(String pseudo, String message);
}
}

Vous noterez que toutes les méthodes sont marquées par la propriété IsOneWay positionnée à true. C'est une bonne pratique concernant les contrats de rappel, car cela permet au service d'effectuer un appel asynchrone sur le client et ainsi de ne pas rester bloqué dessus.

III - Le binding (liaison)

Les contrats établis, nous allons maintenant nous pencher sur le binding (liaison) utilisé par notre service. Nous verrons plus en détail les notions de communication unidirectionnelle et duplex que nous avions mentionnées dans la partie consacrée aux contrats.

III-A - Communication unidirectionnelle et duplex

WCF offre trois types de modèles de message : requête/réponse, unidirectionnelle et duplex.

Le modèle requête/réponse est celui utilisé par défaut dans WCF. Ici le client envoie une requête au serveur et attend une réponse de celui-ci. Tant que la réponse n'est pas arrivée, le client reste bloqué. Cela se passe même lors de l'appel à une méthode ne renvoyant pas de résultat (void). En effet, le serveur renvoie quand même un message vide pour indiquer au client que la méthode a été retournée.

Le modèle unidirectionnel permet d'appeler des méthodes (uniquement void) sans devoir attendre de réponse en retour. Cela correspond à un appel asynchrone, et le client n'est pas bloqué. L'inconvénient est que le client n'est pas informé d'une éventuelle erreur (levée d'une exception par exemple).

Pour rendre une opération unidirectionnelle, il suffit de positionner la propriété IsOneWay de l'attribut OperationContract à true (comme nous l'avons vu dans la partie consacrée aux contrats). Bien entendu, l'opération en question ne devra renvoyer aucun résultat (void).

Le modèle duplex permet aux clients et aux serveurs de communiquer entre eux (le client appelant le serveur, et le serveur rappelant le client). Pour utiliser ce modèle, il est nécessaire de créer un contrat de service auquel on associe un contrat de rappel. Ce dernier devra être implémenté par le client afin que le serveur sache comment le rappeler.

Notre chat sera basé sur le modèle duplex que nous combinerons avec le modèle unidirectionnel (c'est en général le cas). En effet, les requêtes du client et les rappels du serveur seront en fait des opérations unidirectionnelles. Cela permettra au client de ne pas rester bloqué lorsqu'il appelle le serveur, mais aussi au serveur de ne pas rester bloqué quand il rappellera le client !

III-B - Choix du binding

La sélection du binding (liaison) c'est-à-dire du protocole de communication à utiliser entre le service et ses clients va en partie dépendre des fonctionnalités que nous souhaitons voir sur le service (prise en charge des sessions, communication duplex, sécurité, etc.).

Le tableau suivant présente la liste des liaisons disponibles avec WCF et leurs fonctionnalités (seulement celles qui présentent un intérêt dans notre cas).

Liaison

Interopérabilité

Session (Par défaut)

Duplex

BasicHttpBinding

Basic Profile 1.1

(Aucun)

n/a

WSHttpBinding

WS

(Aucun), session fiable, session de sécurité

n/a

WSDualHttpBinding

WS

(Session fiable), session de sécurité

Oui

WSFederationHttpBinding

WS-Federation

(Aucun), session fiable, session de sécurité

Non

NetTcpBinding

.NET

(Transport), session fiable, session de sécurité

Oui

NetNamedPipeBinding

.NET

Aucun, (Transport)

Oui

NetMsmqBinding

.NET

(Aucun), Transport

Non

NetPeerTcpBinding

Peer

(Aucun)

Oui

MsmqIntegrationBinding

MSMQ

(Aucun)

n/a

Comme vous pouvez le constater, les seules liaisons offrant à la fois la prise en charge des sessions et de la communication duplex sont WSDualHttpBinding et NetTcpBinding. Pour la construction de notre chat, nous choisirons la liaison NetTcpBinding. Cette dernière est intéressante dans le cas d'applications intranet. Si vous souhaitez héberger l'hôte au sein d'IIS (sans WAS), utilisez plutôt du WSDualHttpBinding. Il est aussi possible d'utiliser le protocole NetPeerTcpBinding (pour construire des applications peer-to-peer), mais son utilisation diffère quelque peu des autres protocoles, c'est pourquoi nous n'en parlerons pas.

IV - Implémentation du service

Nous avons vu précédemment la création du contrat (l'interface IChatWCF) de notre service. Nous allons maintenant passer à son implémentation au travers de la classe ChatWCF.

IV-A - Instanciation et session

Le comportement d'instanciation (défini à l'aide de la propriété InstanceContextMode) contrôle la façon dont les objets du service sont instanciés suite aux requêtes clientes.

WCF supporte trois modes d'instanciation :

- PerCall : un nouvel objet de service est créé à chaque appel du client.

- PerSession : un nouvel objet de service est créé pour chaque nouvelle session cliente et est conservé pendant toute la durée de vie de celle-ci (cela requiert une liaison capable de prendre les sessions en charge). Il s'agit du mode par défaut.

- Single : un objet de service unique gère toutes les demandes du client pendant toute la durée de vie de l'application.

Nous allons choisir pour notre service de chat le mode PerSession. Ainsi, chaque client aura une instance de la classe ChatWCF qui lui sera dédiée.

La définition du mode d'instanciation utilisé par le service se fait au travers de la propriété InstanceContextMode de l'attribut ServiceBehavior :

[ServiceBehavior(
InstanceContextMode = InstanceContextMode.PerSession,
ConcurrencyMode = ConcurrencyMode.Single)]
public class ChatWCF : IChatWCF
{
//code
}

Notez la présence de la propriété ConcurrencyMode dont nous parlerons un peu plus loin.

Le fonctionnement du service (dans le cas de l'envoi d'un message) tel que nous allons l'implémenter peut être modélisé de la façon suivante :

1) Un client C souhaitant transmettre un message appelle la méthode EnvoyerMessage de notre service. Cet appel est traité par l'instance de ChatWCF dédiée à ce client ;

2) Cet objet de service déclenche l'évènement ChatMessageEvent auquel toutes les instances de ChatWCF sont abonnées ;

3) Tous les objets de service sont notifiés de l'envoi d'un nouveau message ;

4) Les différents objets de service transmettent le message à leur client correspondant.

C'est en fait le principe du design pattern observateur qui est ici repris. Ce mode de fonctionnement est utilisé pour les autres méthodes du service (DemarrerSessionet FermerSession). Lors de la création de la session, le nouvel objet ChatWCF s'abonne aux différents évènements. Et lors de la déconnexion du client, l'objet ChatWCF dédié s'en désabonne.

Nous avons donc trois évènements, variables static de la classe ChatWCF :

internal static event ChatMessageEventHandler ChatMessageEvent;
internal delegate void ChatMessageEventHandler(object sender, ChatMessageEventArgs e);

internal static event ConnexionEventHandler ConnexionEvent;
internal delegate void ConnexionEventHandler(object sender, ConnexionEventArgs e);

internal static event DeconnexionEventHandler DeconnexionEvent;
internal delegate void DeconnexionEventHandler(object sender, DeconnexionEventArgs e);

IV-A-1 - Connexion d'un client

La méthode DemarrerSession est la première méthode que peut (et doit) appeler un client. Elle permet d'initialiser la session du client et d'avertir les autres utilisateurs de la connexion d'un nouveau venu.

En voici son code :

public void DemarrerSession(String pseudo)
{
callback = OperationContext.Current.GetCallbackChannel<IChatWCFRappel>();

lock (listeUtilisateurs)
{
pseudoUtilisateur = pseudo;
listeUtilisateurs.Add(pseudo);

//on s'abonne à la réception de nouveaux messages
ChatMessageEvent += ChatMessageHandler;

//on s'abonne à la connexion de nouveaux utilisateurs
ConnexionEvent += ConnexionHandler;

//on s'abonne à la déconnexion des utilisateurs
DeconnexionEvent += DeconnexionHandler;

//on informe les utilisateurs de la connexion d'un nouveau venu
ConnexionEventArgs cea = new ConnexionEventArgs();
cea.Utilisateur = this.pseudoUtilisateur;
ConnexionEvent(this, cea);

Console.WriteLine("Connexion de l'utilisateur {0}", pseudoUtilisateur);
}
}

OperationContext est une classe permettant l'accès au contexte d'exécution d'une méthode d'un service. Sa propriété static Current renvoie le contexte d'exécution pour le thread courant. Il est ainsi possible d'appeler la méthode générique GetCallbackChanne<T> renvoyant le canal de rappel vers le client (pour les communications en duplex). Ce canal est fortement typé. Dans notre cas nous obtenons un objet de type IChatWCFRappel correspondant au contrat de rappel que nous avons créé. Une fois cet objet obtenu, il nous suffira d'appeler ses méthodes (ConnexionUtilisateur, ReceptionMessage, etc.) pour interagir avec le client, facile non ?

Nous récupérons ensuite le pseudo que le client a passé en paramètre et nous le stockons dans la variable de classe pseudoUtilisateur. Nous ajoutons aussi ce pseudo à la liste des pseudos des utilisateurs connectés. Notez que nous ne faisons aucune vérification sur le pseudo (comme l'existence d'un autre utilisateur portant déjà le même) afin de simplifier le code.

Nous nous abonnons ensuite aux différents évènements possibles (connexion d'un utilisateur, réception d'un message et déconnexion d'un utilisateur). À chaque évènement nous associons une fonction de traitement (ChatMessageHandler, ConnexionHandler, DeconnexionHandler).

Une fois l'initialisation de l'instance de ChatWCF effectuée, il reste à informer les autres utilisateurs de la connexion d'un nouveau venu. Pour cela il nous suffit de lever l'évènement ConnexionEvent en lui passant en paramètre un objet ConnexionEventArgs dont voici la définition :

internal class ConnexionEventArgs  : EventArgs
{
public String Utilisateur { get; set; }
}

Tous les abonnés (c'est-à-dire toutes les instances de ChatWCF) à cet évènement seront donc notifiés de la connexion. Les fonctions de traitements associées (ConnexionHandler) seront donc exécutées sur chaque abonné. Voici le code de cette fonction :

//méthode appelée quand l'évènement ConnexionEvent est déclenché
private void ConnexionHandler(object sender, ConnexionEventArgs e)
{
callback.ConnexionUtilisateur(e.Utilisateur);
}

Chaque instance ChatWCF va donc rappeler son client associé via le contrat de rappel. Ici, nous appelons la méthode ConnexionUtilisateur en lui passant en paramètre le pseudo de l'utilisateur connecté. Le pseudo est récupéré via l'objet ConnexionEventArgs vu au-dessus.

IV-A-2 - Envoi d'un message

Voyons maintenant l'envoi de messages (toujours côté serveur). Le client désirant envoyer un message va utiliser la méthode EnvoyerMessage qui prend en paramètre ledit message :

public void EnvoyerMessage(string message)
{
//on informe les utilisateurs d'un nouveau message
ChatMessageEventArgs mea = new ChatMessageEventArgs();
mea.Message = message; //le message
mea.Utilisateur = this.pseudoUtilisateur; //l'expéditeur
ChatMessageEvent(this, mea);
}

Nous construisons tout d'abord un objet de type ChatMessageEventArgs qui va contenir le message à envoyer aux clients et le pseudo de l'expéditeur. Le pseudo de l'expéditeur correspond à la variable pseudoUtilisateur de l'instance ChatWCF qui traite l'envoi du message. Cela est bien sûr possible, car nous utilisons des sessions (un objet ChatWCF différent est affecté à chaque client).

Puis nous déclenchons l'évènement ChatMessageEvent en lui passant en paramètre l'objet ChatMessageEventArgs dont voici la définition :

internal class ChatMessageEventArgs : EventArgs
{
public String Message { get; set; }
public String Utilisateur { get; set; } //expéditeur du message
}

Tous les abonnés à l'évènement ChatMessageEvent seront donc notifiés de son déclenchement. Il en résultera l'appel à la méthode ChatMessageHandler sur tous ces abonnés :

//méthode appelée quand l'évènement ChatMessageEvent est déclenché
private void ChatMessageHandler(object sender, ChatMessageEventArgs e)
{
callback.ReceptionMessage(e.Utilisateur, e.Message);
}

Cette méthode utilise le canal de rappel pour appeler la méthode ReceptionMessage sur le client.

IV-A-3 - Fermeture de session

La troisième et dernière méthode de notre service concerne la fermeture de session. La méthode FermerSession est appelée par le client lorsqu'il souhaite se déconnecter.

public void FermerSession()
{
lock (listeUtilisateurs)
{
//on désabonne le client
ChatMessageEvent -= chatMessageHandler;
ConnexionEvent -= connexionHandler;
DeconnexionEvent -= deconnexionHandler;

listeUtilisateurs.Remove(this.pseudoUtilisateur);
}

//on informe les utilisateurs de la déconnexion
DeconnexionEventArgs dea = new DeconnexionEventArgs();
dea.Utilisateur = this.pseudoUtilisateur;

if(DeconnexionEvent != null)
DeconnexionEvent(this, dea);

Console.WriteLine("Déconnexion de l'utilisateur {0}", this.pseudoUtilisateur);
}

Nous utilisons la même technique que précédemment en déclenchant l'évènement DeconnexionEvent et en lui passant en paramètre un objet DeconnexionEventArgs contenant le pseudo de l'utilisateur se déconnectant.

internal class DeconnexionEventArgs : EventArgs
{
public String Utilisateur { get; set; }
}

Le déclenchement de cet évènement provoquera l'appel à la fonction DeconnexionHandler :

//méthode appelée quand l'évènement DeconnexionEvent est déclenché
private void DeconnexionHandler(object sender, DeconnexionEventArgs e)
{
callback.DeconnexionUtilisateur(e.Utilisateur);
}

Comme d'habitude, nous utilisons la référence sur le canal de rappel afin d'appeler le client via la fonction DeconnexionUtilisateur.

IV-B - Concurrence

Nous allons ici aborder le délicat sujet de la concurrence. Nous allons devoir gérer deux types de concurrences au sein de notre service : l'accès concurrentiel sur une instance d'un objet de service et l'accès concurrentiel aux ressources partagées (la liste des pseudos des utilisateurs connectés). WCF permet de gérer le premier, le deuxième devra l'être par le développeur.

La propriété ConcurrencyMode de l'attribut ServiceBehavior permet de contrôler le nombre de threads actifs sur une instance d'un objet de service.

Cette propriété peut prendre trois valeurs différentes :

- Single : un seul thread à la fois peut accéder à un objet de service. Les autres threads qui souhaitent accéder à l'instance doivent se bloquer jusqu'à ce que le thread d'origine quitte l'instance.

- Multiple : chaque objet de service peut avoir plusieurs threads qui traitent des messages simultanément.

- Rentrant : chaque objet de service traite un seul message à la fois, mais accepte les appels rentrants.

Notre service est configuré sur le mode Single. C'est-à-dire qu'un objet de service ne peut traiter qu'un seul message à la fois. Ainsi nous réduisons les problèmes de concurrence quant à l'accès aux objets de service. Mais comprenez bien que cela ne veut pas dire que le service ne pourra pas traiter plus d'un seul message à la fois ! En effet, nous avons vu que le mode d'instanciation était positionné sur PerSession. Nous avons donc un objet de service par client connecté. Notre service ne peut donc pas traiter plusieurs requêtes d'un même client en même temps, mais peut tout à fait traiter plusieurs clients différents en même temps.

Chaque objet de service est donc protégé contre les accès concurrents. Cependant plusieurs objets de service peuvent vouloir accéder en même temps à des ressources partagées (objet static par exemple). Il convient donc de protéger l'accès à ces ressources. Dans le cas de notre service nous avons une liste static contenant les pseudos des utilisateurs connectés. Cette liste est mise à jour à chaque connexion ou déconnexion d'un utilisateur. Il faut donc protéger cette liste des accès de threads concurrents pendant qu'un thread est en train de la manipuler.

Le Framework .Net propose différentes techniques de synchronisation. Vous pouvez par exemple utiliser le mot clef lock.

Dans notre cas, la protection de la liste static pourrait s'écrire de la façon suivante :

lock (listeUtilisateurs)
{
listeUtilisateurs.Add(pseudo);
}

L'instruction lock prenant ici en paramètre la ressource static.

V - L'hébergement du service

V-A - Présentation

Maintenant que notre service WCF est implémenté, il nous faut maintenant l'héberger. La plateforme NET offre différentes solutions d'hébergements, chacune ayant ses avantages et ses inconvénients. Il vous est possible d'utiliser IIS (pour du protocole http), WAS avec IIS7 (permettant de prendre en charge tout type de protocole) ou bien d'utiliser l'autohébergement. L'autohébergement est la solution la plus simple, mais aussi la plus limitée pour l'hébergement des services WCF. Elle consiste à utiliser une application managée pour héberger le service. Cela peut être une application console, une application WinForm ou WPF, ou bien un service Windows. Il est alors de votre responsabilité d'écrire le code nécessaire à la gestion de la durée de vie du service (instanciation, arrêt).

Dans notre cas nous allons utiliser une application console pour la bonne et simple raison qu'il s'agit de la solution la plus simple à mettre en place pour tester notre application compte tenu du protocole utilisé. Une solution intéressante dans le cas d'un chat utilisé dans un intranet aurait été le recours à un hébergement dans un service Windows.

V-B - Zoom sur le code

Créons donc tout d'abord un projet de type Application Console. Il nous faut ensuite ajouter les références nécessaires au projet. Comme nous faisons du WCF, il faut obligatoirement référencer la dll System.ServiceModel. Ensuite nous avons besoin des références du service que l'on va héberger. Ajoutons donc les références sur les dll des contrats (ChatContrats) et de leurs implémentations (ChatServices).

Enfin, ajoutez un fichier de configuration (App.config) qui permettra de paramétrer l'hébergement du service.

Nous pouvons maintenant passer au code. Ici trois lignes de code suffisent pour initialiser le service :

Code de l'hôte

static void Main(string[] args)
{
//démarrage du service
using (ServiceHost hote = new ServiceHost(typeof(ChatServices.ChatWCF)))
{
hote.Open();
Console.WriteLine("Service démarré");
Console.ReadLine();
}
}

Nous créons tout d'abord une instance de ServiceHost (auquel tout service doit être associé afin d'être accessible) en lui passant en paramètre le type du service à héberger (le type concret, pas le contrat !). L'appel à la méthode Open initialise le ServiceHost et le met à l'écoute des différents points de communication configurés dans le fichier de configuration (que nous allons voir juste après). L'instruction Console.ReadLine() permet de laisser la console ouverte (astuce classique).

Vous remarquerez que nous n'avons pas utilisé la méthode Close(). Elle n'est ici pas indispensable, car nous avons utilisé l'instruction using qui se charge de détruire proprement l'objet ServiceHoste lorsque la console se fermera.

Jetons maintenant un coup d'œil au fichier de configuration associé :

Fichier de configuration de l'hôte

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="ChatServices.ChatWCF">
<endpoint address="ChatWCF"
binding="netTcpBinding"
contract="ChatContrats.IChatWCF">
</endpoint>
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:8732/ChatServices/" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>

Nous déclarons ici un service de type ChatServices.ChatWCF via l'élément service. Celui-ci exposera un point de communication pour lequel nous spécifions les fameux ABC (address, binding contract) de WCF. Vous remarquerez que l'adresse (ChatWCF) est un peu spéciale. En effet, c'est une adresse relative. Oui, mais relative à quoi ? Et bien à l'adresse de base associée au protocole du point de communication. Ici le protocole est net.tcp et l'adresse de base associée est net.tcp://localhost:8732/ChatServices/. En définitive, notre service sera accessible via l'adresse net.tcp://localhost:8732/ChatServices/ChatWCF. Ce système est très intéressant, car il vous permet de ne changer que l'adresse de base (à un seul endroit donc) lorsque le service change de machine d'hébergement (par exemple pour passer d'une machine de développement à une machine de production).

VI - Le client WinForm

VI-A - Présentation

L'interface de l'application cliente est assez simple et tient en une fenêtre dont voici une capture écran :

Un contrôle richTextBox permet l'affichage des messages, tandis qu'une textBox (en bas) sert à saisir les messages à envoyer. Inutile de vous décrire l'utilité du bouton Envoyer.

Un menu en haut à gauche permet de se connecter ou de se déconnecter au service. L'utilisation du menu Se connecter affichera la boite de dialogue suivante permettant de saisir son pseudo :

VI-B - Référence au service

Il existe deux façons différentes de référencer un service WCF. La première est de fournir directement au client l'assembly contenant les contrats du service. Le client n'a plus qu'à les référencer et à écrire le code permettant la création du canal de communication vers le serveur.La deuxième façon est de publier les métas-datas des services WCF sous forme de WSDL SOAP. Le client les utilisera pour générer le proxy lui permettant d'utiliser les services. Dans notre exemple nous utiliserons la première solution.

Ainsi, pour pouvoir utiliser notre service de chat, il va nous falloir ajouter deux références nécessaires au projet. Comme nous faisons du WCF, il faut obligatoirement référencer la dll System.ServiceModel. Ensuite nous avons besoin de référencer l'assembly contenant les contrats (ChatContrats) du service que l'on va héberger (mais pas l'assembly de son implémentation).

Ensuite, il nous faut créer un fichier de configuration (App.config) permettant de stocker la configuration pour l'utilisation du service. Voici le contenu de fichier :

Configuration client

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.tcp://localhost:8732/ChatServices/ChatWCF/"
binding="netTcpBinding"
contract="ChatContrats.IChatWCF"
name="configClient" />
</client>
</system.serviceModel>
</configuration>

Enfin, dans le code de l'application, n'oubliez pas la directive using :

using ChatContrats;
using System.ServiceModel;

VI-C - Appel au service

La classe de la fenêtre de discussion se nomme DiscussionForm. Nous allons lui faire implémenter l'interface IChatWCFRappel. Souvenez-vous, il s'agit du contrat de rappel que nous avions créé plus haut et qui permet au serveur de rappeler le client.

Nous créons trois variables de classe :

- pseudoUtilisateurCourant permettant de stocker le pseudo saisi dans la fenêtre de connexion ;

- channelFactory est un générateur de canaux. Il s'agit ici d'un objet de type DuplexChannelFactory permettant, comme son nom l'indique, de gérer les communications duplex ;

- chat représentant le canal permettant d'accéder au service. Il s'agit d'un objet fortement typé IChatWCF, c'est-à-dire le type correspondant au contrat de notre service.

public partial class DiscussionForm : Form, ChatContrats.IChatWCFRappel
{
String pseudoUtilisateurCourant;
DuplexChannelFactory<ChatContrats.IChatWCF> channelFactory;
IChatWCF chat;

public DiscussionForm()
{
InitializeComponent();
}
//reste du code}

VI-C-1 - Connexion du client

Un clic sur le menu Se connecter appelle le la fenêtre de connexion puis la méthode ConnexionChat en lui passant en paramètre le pseudo saisi.

private void seConnecterToolStripMenuItem_Click(object sender, EventArgs e)
{
ConnexionForm connect = new ConnexionForm();
if (connect.ShowDialog() == DialogResult.OK)
{
ConnexionChat(connect.Pseudo);
}
}

La méthode ConnexionChatinstancie la variable channelFactory. Une des différentes versions du constructeur de DuplexChannelFactory prend en paramètre l'objet implémentant l'interface de rappel (en l'occurrence this) et une chaine de caractères représentant le nom de la configuration du point de communication (contenu dans le fichier de configuration du client).Une fois le générateur de canaux instancié, nous pouvons nous en servir pour créer un canal client. Pour cela, un simple appel à la méthode CreateChannel suffit. Nous obtenons en retour un objet de type IChatWCF sur lequel nous pouvons appeler les méthodes du service.

Nous appelons donc ensuite la méthode DemarrerSession afin d'initialiser la session de l'utilisateur sur le serveur.

Voici le code de la méthode ConnexionChat :

private void ConnexionChat(string pseudo)
{
if (String.IsNullOrEmpty(pseudo))
return;

//création du canal avec la configuration définie dans le fichier de config
channelFactory = new DuplexChannelFactory<ChatContrats.IChatWCF>(this, "configClient");

chat = channelFactory.CreateChannel();

try
{
chat.DemarrerSession(pseudo);

//mise à jour des menus
this.seConnecterToolStripMenuItem.Enabled = false;
this.seDéconnecterToolStripMenuItem.Enabled = true;
this.btnEnvoyer.Enabled = true;

pseudoUtilisateurCourant = pseudo;

}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

VI-C-2 - Déconnexion du client

La déconnexion du client n'est pas plus compliquée. Après un peu de nettoyage au sein de l'interface graphique, nous appelons la méthode FermerSession de l'objet chat pour terminer la session côté serveur puis la méthode Close de l'objet channelFactory afin de fermer le canal.

private void DeconnexionChat()
{
//on efface les messages à l'écran
richTextBox1.Clear();
txtMessage.Clear();
pseudoUtilisateurCourant = null;

//mise à jour des menus
this.seDéconnecterToolStripMenuItem.Enabled = false;
this.seConnecterToolStripMenuItem.Enabled = true;
this.btnEnvoyer.Enabled = false;

//on se déconnecte du service. le canal devient inutilisable
try
{
if (chat != null)
chat.FermerSession();
if (channelFactory != null)
channelFactory.Close();
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
}

VI-C-3 - Envoi d'un message

Maintenant que l'on sait se connecter et se déconnecter du service, voyons comment envoyer des messages (vous allez voir, c'est extrêmement difficile). L'appui sur le bouton Envoyer appelle la fonction EnvoyerMessageServeur prenant en paramètre le message à envoyer :

//permet l'envoi d'un message
private void EnvoyerMessageServeur(string message)
{
try
{
//on vérifie l'état du canal
if (channelFactory.State != CommunicationState.Faulted)
{
chat.EnvoyerMessage(message);
}
else
{
MessageBox.Show("Le canal de communication est défaillant. Vous allez être déconnecté",
"Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error);
DeconnexionChat();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

Nous vérifions en premier l'état du canal de communication (ce qui devrait être fait avant chaque appel). Puis nous appelons la méthode EnvoyerMessage de l'objet chat en lui passant en paramètre le message à transmettre. Et c'est tout.

VI-D - Implémentation du contrat de rappel

À ce stade, nous venons de faire la moitié du travail côté client. Nous savons en effet communiquer avec le serveur afin de se connecter, déconnecter et envoyer des messages. Il reste maintenant à configurer le client afin que le serveur puisse dialoguer avec (en utilisant le contrat de rappel). Pour cela il faut que le client implémente l'interface IChatWCFRappelet ses trois méthodes : ConnexionUtilisateur, DeconnexionUtilisateur et ReceptionMessage.

VI-D-1 - Connexion d'un autre utilisateur

Lorsqu'un utilisateur se connecte au service, celui-ci en informe les utilisateurs connectés en utilisant la méthode ConnexionUtilisateur du contrat de rappel (et qui prend en paramètre le pseudo du nouveau connecté).

Côté client nous implémentons cette méthode de la façon suivante :

public void ConnexionUtilisateur(string pseudo)
{
if (!String.IsNullOrEmpty(pseudo))
{
richTextBox1.AppendText(String.Format("*** {0} - {1} vient de se connecter ***", DateTime.Now.ToLongTimeString(), pseudo));
richTextBox1.AppendText("\n");
}
}

Après avoir vérifié que le pseudo n'est pas vide ou null, nous affichons à l'écran (via le richTextBox) un message indiquant la connexion d'un nouvel utilisateur.

VI-D-2 - Déconnexion d'un autre utilisateur

La déconnexion d'un utilisateur suit le même principe pour la connexion : nous affichons un message à l'écran :

public void DeconnexionUtilisateur(string pseudo)
{
if (!String.IsNullOrEmpty(pseudo))
{
richTextBox1.AppendText(String.Format("*** {0} - {1} vient de se déconnecter ***", DateTime.Now.ToLongTimeString(), pseudo));
richTextBox1.AppendText("\n");
}
}

VI-D-3 - Réception d'un message

Pour la réception d'un nouveau message nous restons dans le classique avec l'affichage du message et du pseudo de son expéditeur à l'écran :

public void ReceptionMessage(string pseudo, string message)
{
if (!String.IsNullOrEmpty(pseudo) && !String.IsNullOrEmpty(message))
{
richTextBox1.AppendText(String.Format("{0} - {1} dit: ", DateTime.Now.ToLongTimeString(), pseudo));
richTextBox1.AppendText(message);
richTextBox1.AppendText("\n");
}
}

VI-D-4 - Exemple en image

Voici un exemple de conversation entre deux utilisateurs. Vous pouvez y voir les différentes utilisations possibles du service : connexion, déconnexion et envoi de message.

Conclusion

Nous voilà arrivés à la fin de cet article. Nous avons pu y aborder et mettre en application un certain nombre de notions importantes de WCF. Notre chat, bien que fonctionnel n'en est pas pour autant terminé. De nombreux points seraient à améliorer, à commencer par la sécurité. Alors, à vous de jouer.

Liens

Introduction à Windows Communication Foundation Par Vincent Lainé

Introduction à Windows Communication Foundation Par Ronald VASSEUR

Windows Communication Foundation sur MSDN

Hosting and Consuming WCF Services

Secure Hosting and Deployment of WCF Services

Programming Indigo : Contracts

Le blog dédié au livre Introduction à WCF

Sources

Téléchargez les sources de la solution donnée en exemple.

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Contact

Si vous constatez une erreur dans le tutoriel, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.