Récemment Julien m’a demandé comment il était possible de mettre un place un système de communication bi-directionnel en WCF. Comme j’avais un peu de temps, je lui ai fais une petite application de démonstration dont je partage avec vous les quelques subtilités.
L’idée est de réaliser une application très basique avec 2 fonctionnalités :
- n clients se connectant en fournissant leur nom (Abonnement au serveur)
- 1 serveur capable d’émettre des messages (via la console) à tous ses clients abonnés.
L’implémentation se fait en 3 grandes étapes correspondant chacune à un projet différent
1ère étape : Définir les interfaces de communication (Projet Interfaces)
Comme toujours en WCF, on commence par définir les interfaces de communication.
Il nous faut 1 interface qui corresponde aux échanges du client vers le serveur (Abonnement d’un client) :
[ServiceContract(CallbackContract = typeof(ICallBack))]
public interface IServeur
{
[OperationContract()]
void Connecter(string nom);
}
[/sourcecode]
Hormis les traditionnels attributs ServiceContract & OperationContrat, on notera la présence de la propriété CallbackContract permettant de définir l’interface que pourra utiliser le serveur pour rappeler ses clients :
public interface ICallBack
{
[OperationContract()]
void RecevoirMessage(string message);
}
[/sourcecode]
Dans notre cas, le serveur appellera la méthode RecevoirMessage de chacun de ses clients en lui passant le message émis.
2ème étape : Créer le serveur (Projet Serveur)
Côté serveur, on dispose de 2 classes :
- Program : Gestion de la console et hébergement du serveur via le classique ServiceHost
static void Main()
{
Console.WriteLine("Démarrage du serveur");
Serveur svr = new Serveur();
using (ServiceHost host = new ServiceHost(svr))
{
host.Open();
Console.WriteLine("Entrez un message (‘fin’ pour terminer)");
string msg = null;
while ((msg = Console.ReadLine()) != "fin")
{
svr.EmettreMessage(msg);
// peut être écrit aussi : (host.SingletonInstance as Serveur).EmettreMessage(msg);
}
}
}
[/sourcecode]
A chaque fois qu’un message est tapé sur la console, celui-ci est ré-émis vers tous les clients (Méthode EmettreMessage)
Pour simplifier la configuration du serveur a été faite avec l’outil de configuration WCF accessible via un clic droit sur le fichier app.Config, option “Modifier la configuration WCF” :
- Serveur :
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class Serveur : IServeur
{
Dictionary<string, ICallBack> _lstCallBacks = new Dictionary<string,ICallBack>();
public void Connecter(string nom)
{
Console.WriteLine("Connection de " + nom);
_lstCallBacks.Add(nom, OperationContext.Current.GetCallbackChannel<ICallBack>());
}
public void EmettreMessage(string msg)
{
foreach (ICallBack cb in _lstCallBacks.Values)
{
cb.RecevoirMessage(msg);
}
}
}
[/sourcecode]
Ce code présente les particularités suivantes :
- Présence de l’attribut ServiceBehavior sur la classe avec la propriété InstanceContexteMode positionnée à Single: Le service est alors exposé en tant que singleton. C’est toujours la même classe qui est appelée et qui contient tous les callbacks des clients.
- _lstCallBacks : la liste des tous les clients abonnés. Chaque instance correspond à un proxy vers le client et a été récupérée pendant la connexion du client via la méthode OperationContext.Current.GetCallbackChannel<ICallBack>().
- La méthode EmettreMessage se contente ensuite de boucler sur la liste des CallBacks pour émettre les messages vers les clients. C’est la partie techniquement la plus compliquée mais heureusement pour nous, c’est WCF qui se charge de tout
3ème étape : Créer le client (Projet client)
Pour simplifier la démo (et le déboguage), une seule application a été créée pour simuler 4 clients :
2 points importants sur cette application :
- La classe CallBack implémentant l’interface ICallBack : c’est la classe qui sera appelée par le serveur. L’objet CallBack côté serveur correspond à un proxy vers une instance de CallBack côté client.
- La “plomberie” WCF. Toute l’initialisation est faite par code (et non par paramétrage : je vous le laisse en exercice ) :
var factory = new DuplexChannelFactory<IServeur>(_lstCallBacks[nom]);
factory.Endpoint.Address = new EndpointAddress("net.tcp://localhost:12345");
factory.Endpoint.Binding = new NetTcpBinding();
factory.Open();
var c = factory.CreateChannel(new EndpointAddress("net.tcp://localhost:12345"));
c.Connecter(nom);
[/sourcecode]
Télécharger le projet
Pour plus de détails, n’hésitez pas à télécharger le projet ici.
Application de folie !
Petite précision : dans le cas où votre service web expose un traitement assez long, il est fort possible que votre appel plante avec un message du genre « Le service web ne répond pas » (même si on fixe un très grand timeout).
Pour contrer cela, vous avez la possibilité de préciser [OperationContract(IsOneWay = true)] sur la méthode de l’interface IServeur. Cela permettra au client de faire l’appel au serveur mais sans attendre sa réponse (implique que si l’appel n’aboutit pas, le client n’en sera jamais informé). Jusqu’à maintenant, je n’ai pas trouvé mieux …