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 :
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 :
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é :
<?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 :
<?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
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.