Utiliser Rx dans une application client-serveur bi-directionnelle avec Silverlight et WCF

9 minutes read

Dans l’article précédent je vous avais montré comment Rx pouvait améliorer le processus de chargement de paquets de données depuis un service WCF. C’était un scénario typique où le client Silverlight demandait au serveur des données. Maintenant nous allons voir comment on peut utiliser Rx dans un scénario où c’est le serveur qui envoie directement des données au client sans que ce dernier en ait fait explicitement la demande.

Les pré-requis

Afin de bien suivre cet article vous devez vous assurer d’avoir les dernières versions de Rx et du SDK de Silverlight. Au moment où ces lignes ont été écrites j’utilisais Silverlight 4.0.60310.0 et Rx 1.0.10.621.0.

Le contexte

On veut créer une application Silverlight et un service WCF qui pousse des données de type température au client à intervalles plus ou moins réguliers. L’unité utilisée sera les degrés Celsius. L’application Silverlight (alias client) est divisée en deux modules indépendants, un pour afficher les températures reçues en Celsius et l’autre pour les afficher en Fahrenheit. Même si on a deux modules, on veut qu’ils utilisent la même connexion au service WCF pour recevoir les températures. Ces modules devront aussi n’avoir aucune référence directe sur le proxy WCF généré par Visual Studio de façon à permettre l’injection de dépendance ou encore le test unitaire des view models des modules.

Services duplex

Un service duplex est un service où à la fois le client et le serveur peuvent envoyer des données via la même connexion, ce qui est exactement ce dont nous avons besoin ici. Nous avons choisi de créer un service en utilisant le pattern Subscribe-Publish. Le client créera une connexion au serveur en utilisant la méthode Subscribe de ce dernier et attendra ensuite de manière asynchrone que le serveur lui envoie des données.

En WCF, ce type de scénario est rendu possible par l’utilisation d’une callback de service (service callback en anglais). Cette callback est une interface définie côté serveur et implémentée côté client que le serveur utilisera à chaque fois qu’il souhaite envoyer des données au client. Pour pouvoir utiliser ces callbacks on doit choisir un binding WCF compatible. Quand on crée une application WPF on peut utiliser tous les bindings fournis par défaut tel que le wsDualHttpBinding mais en Silverlight nous sommes plus limités. En Silverlight on peut utiliser le netTcpBinding ou le pollingDuplexHttpBinding. Le premier est un peu plus compliqué à configurer que le second aussi il fera l’objet d’un article séparé. Pour aujourd’hui on va se concentrer sur le second.

PollingHttpDuplexBinding

Ce binding est un peu spécial car c’est un binding duplex fonctionnant via le protocole http or il est bien connu que ce protocole n’est pas bi-directionnel. En http, le client doit faire une demande au serveur pour recevoir des données. Le serveur ne peut pas de lui-même envoyer des données au client comme il est possible de le faire avec des sockets. Si on a besoin que le serveur envoie des données au client on peut tricher en mettant en place ce que l’on appelle du polling. Le polling est le processus où le client appelle régulièrement le serveur (toutes les secondes par exemple) pour savoir si de nouvelles données sont disponibles ou non. C’est un peu un dialogue du genre de l’âne dans Shrek (c’est quand qu’on arrive ?) :

Client : Hey t’as des nouvelles choses pour moi ? Serveur : Non Client : Et maintenant ? Serveur : Non je t’ai déjà dis ça il y a une seconde ! Client : Ouais mais tu me le dis jamais toi-même je dois toujours te le demander. Alors quelque-chose ? Serveur : Non ! … 30 essais plus tard Client : Allez et maintenant ? Serveur : Ouais ça y est j’ai quelque-chose, tiens voilà.

Vous remarquez donc que ce processus utilise pas mal d’appels réseaux. Tout réside donc dans la bonne optimisation de ce dernier et dans le bon choix d’intervalle entre les appels au serveur pour éviter de le surcharger. Le pollingHttpDuplexBinding implémente déjà tout ce polling directement au sein de la couche réseau de Silverlight améliorant ainsi nettement les performances de l’ensemble (pas de retours permanents sur le dispatcher). Charge maintenant à nous de choisir le bon intervalle de temps entre les appels en fonction du nombre de clients et de la réactivité attendue par l’utilisateur de l’application Silverlight. En effet, un jeu demandera une grande réactivité alors qu’un logiciel de chat beaucoup moins car recevoir son message au bout de 3 secondes à la place de 1 seconde ne change la vie de personne mais diminue assez nettement la charge à supporter par le serveur.

Le pollingHttpDuplexBinding n’est pas inclus par défaut dans Silverlight ou WCF. On peut trouver les assemblys nécessaires dans le SDK de Silverlight. Sur mon poste elle se trouvent dans C:\Program Files (x86)\Microsoft SDKs\Silverlight\v4.0\Libraries alors pour vous ca devrait être un truc du genre C:[ProgramFilesArchitecture]\Microsoft SDKs\Silverlight[SilverlightVersion]\Libraries.

Il y a deux assemblys requises pour faire fonctionner ce binding, une pour le client et une autre pour le serveur. Dans le projet web hébergeant l’application Silverlight et vos services WCF ajoutez une référence à la dll serveur de System.ServiceModel.PollingDuplex.dll et dans le projet Silverlight ajoutez une référence à la dll client.

Création du service

Dans le projet web on commence par créer deux interfaces définissant les contrats WCF pour le service de température et sa callback.

[ServiceContract(CallbackContract = typeof(ITemperatureServiceCallback))]
public interface ITemperatureService {
    [OperationContract(IsOneWay = true)]
    void Subscribe();

    [OperationContract(IsOneWay = true)]
    void Unsubscribe();
}
public interface ITemperatureServiceCallback {
    [OperationContract(IsOneWay = true)]
    void PushTemperature(double temperature);
}

On peut ensuite créer une implémentation du service (l’implémentation ici est simple et ne suffirait bien entendu pas à des services de production). Cette implémentation crée un timer qui appellera la callback de tous les clients connectés au serveur pour envoyer des données de type températures générées aléatoirement à intervalle de temps plus ou moins régulier.

public class TemperatureService : ITemperatureService {
    private static readonly object _locker = new object();
    private static readonly List<OperationContext> _clients = new List<OperationContext>();
    private static readonly Random _random = new Random();

    private static Timer _updateTimer;

    static TemperatureService()
    {
        _updateTimer = null;
    }

    public void Subscribe()
    {
        if (_clients.Contains(OperationContext.Current) == false)
        {
            lock (_locker)
            {
                if (_clients.Count == 0)
                    _updateTimer = new Timer(TimerTick, null, 500, 2000);

                _clients.Add(OperationContext.Current);
            }
        }
    }

    public void Unsubscribe()
    {
        RemoveClient(OperationContext.Current);
    }

    private static void TimerTick(object state)
    {
        Task.Factory.StartNew(() =>
        {
            double temperature = _random.Next(-200, 400) * 0.1d;

            // Copy the clients array because it can be modified while been read var clients = _clients.ToArray();
            foreach (var client in clients)
            {
                try {
                    var channelState = client.Channel.State;
                    if (channelState == CommunicationState.Opened)
                    {
                        var callbackChannel = client.GetCallbackChannel<ITemperatureServiceCallback>();
                        callbackChannel.PushTemperature(temperature);
                    }
                    else {
                        RemoveClient(client);
                    }
                }
                catch (TimeoutException)
                {
                    RemoveClient(client);
                }
                catch (Exception)
                {
                    _updateTimer.Dispose();
                }
            }
        });
    }

    private static void RemoveClient(OperationContext client)
    {
        lock (_locker)
        {
            _clients.Remove(client);
        }
    }
}

Dans cette implémentation vous devez porter votre attention sur deux choses :

  • OperationContext.Current
  • client.GetCallbackChannel

OperationContext.Current nous donne des informations sur le client qui a appelé la méthode de service en cours d’exécution. client.GetCallbackChannel nous donne une instance de la callback de service à utiliser pour envoyer des données au client.

Ensuite on expose le service en utilisant un fichier svc. Le mien est le suivant :

<%@ ServiceHost Language="C#" Debug="true" Service="SilverlightReactivePushServer.Web.TemperatureService" CodeBehind="TemperatureService.svc.cs" %>

Finalement on doit configurer le binding dans le fichier web.config, voici la section serviceModel pour le code de ce projet :

<system.serviceModel>
  <extensions>
    <bindingExtensions>
      <add name="pollingDuplexHttpBinding"
           type="System.ServiceModel.Configuration.PollingDuplexHttpBindingCollectionElement, System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
     </bindingExtensions>
  </extensions>
  <bindings>
  <pollingDuplexHttpBinding>
    <binding name="multipleMessagesPerPollPollingDuplexHttpBinding"
                   duplexMode="MultipleMessagesPerPoll"
                   maxOutputDelay="00:00:00.500"
                   sendTimeout="00:00:02.000"
                   closeTimeout="00:00:02.000"/>
    </pollingDuplexHttpBinding>
  </bindings>
  <services>
    <service name="SilverlightReactivePushServer.Web.TemperatureService">
      <endpoint address="" binding="pollingDuplexHttpBinding"
                bindingConfiguration="multipleMessagesPerPollPollingDuplexHttpBinding"
                name="pollingDuplex"
                contract="SilverlightReactivePushServer.Web.ITemperatureService" />
      <endpoint address="mex" binding="mexHttpBinding" name="mex" contract="IMetadataExchange" />
    </service>
  </services>
  <behaviors>
    <serviceBehaviors>
      <behavior name="">
        <serviceMetadata httpGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="false" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Nous en avons fini pour la partie serveur.

Utilisation du service en Silverlight

Premièrement, on va ajouter une référence sur le service que nous avons créé précédemment. Choisissons comme espace de nom TemperatureServer.

L’outil utilisé par Visual Studio pour générer le proxy de service génère correctement les classes requises mais échoue à générer un fichier ServiceReferences.ClientConfig correct. Par correct, je veux simplement dire que le fichier généré est vide.

On va donc écrire la configuration nous même (n’oubliez pas de remplacer les adresses des endpoint par les vôtres).

<configuration>
  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="httpPolling">
          <binaryMessageEncoding />
          <pollingDuplex duplexMode="MultipleMessagesPerPoll" />
          <httpTransport transferMode="StreamedResponse" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
        </binding>
      </customBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:1614/TemperatureService.svc"
                binding="customBinding"
                bindingConfiguration="httpPolling"
                contract="TemperatureServer.ITemperatureService" />
    </client>
  </system.serviceModel>
</configuration>

On va maintenant créer une nouvelle classe nommée TemperatureService qui englobera le proxy WCF et l’exposera sous forme de collection observable. Cette classe est un singleton (libre à vous d’utiliser de l’injection de dépendance à la place du singleton) qui sera utilisé par tous les modules (Celsius et Fahrenheit) pour appeler le service :

public class TemperatureService {
    private static readonly TemperatureService _temperature = new TemperatureService();

    private TemperatureServiceClient _client;
    private readonly IObservable<double> _temperatures; 

    protected TemperatureService()
    {
        _temperatures = Observable.Create<double>(observer =>
        {
            if (_client == null)
            {
                _client = new TemperatureServiceClient();
                _client.SubscribeAsync();
            }

            _client.PushTemperatureReceived += (s, a) => observer.OnNext(a.temperature);

            return () => { };
        });
    }

    public IObservable<double> Temperatures
    {
        get { return _temperatures; }
    }

    public static TemperatureService Current
    {
        get { return _temperature; }
    }
}

Dans le constructeur, on crée une observable qui appellera la méthode SubscribeAsync du service WCF une seule fois. Ensuite, à chaque fois qu’un abonnement à l’observable à lieu, on s’abonne à l’évènement PushTemperatureReceived du proxy WCF. La lambda utilisée ici pour s’abonner à l’évènement ne fait rien d’autre que de pousser la valeur reçue dans la collection observable en utilisant la méthode OnNext de son observer. Et voilà, c’est a peu près tout, toute la magie a lieu ici. Maintenant, à chaque fois qu’une température est reçue depuis le serveur, elle sera accessible immédiatement à tous les abonnés de la collection observable.

J’ai créé deux vues pour afficher les températures en Celsius et en Fahrenheit. Voici leurs code XAML :

<UserControl x:Class="SilverlightReactivePushServer.Views.CelsiusView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding CelsiusTemperature}" />
        <TextBlock Text=" °C" />
    </StackPanel>
</UserControl>
<UserControl x:Class="SilverlightReactivePushServer.Views.FahrenheitView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding FahrenheitTemperature}" />
        <TextBlock Text=" °F" />
    </StackPanel>
</UserControl>

Voici comment elles ont été utilisées dans la vue principale :

<UserControl x:Class="SilverlightReactivePushServer.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:views="clr-namespace:SilverlightReactivePushServer.Views">

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <ComboBox Grid.ColumnSpan="2" HorizontalAlignment="Center" ItemsSource="{Binding DuplexTypes}" SelectedItem="{Binding DuplexType, Mode=TwoWay}" />
        <views:CelsiusView Grid.Column="0" Grid.Row="1"  HorizontalAlignment="Right" Margin="5" />
        <views:FahrenheitView Grid.Column="1" Grid.Row="1"  HorizontalAlignment="Left" Margin="5" />
    </Grid>
</UserControl>

Chaque vue de température a son propre vue model dont voici leurs implémentations :

public class CelsiusViewModel : INotifyPropertyChanged {
    private double _celsiusTemperature;
    public event PropertyChangedEventHandler PropertyChanged;

    public double CelsiusTemperature
    {
        get { return _celsiusTemperature; }
        private set {
            if (_celsiusTemperature != value)
            {
                _celsiusTemperature = value;
                RaisePropertyChanged("CelsiusTemperature");
            }
        }
    }

    public CelsiusViewModel()
    {
        TemperatureService.Current.Temperatures.Subscribe(t => CelsiusTemperature = t);
    }

    private void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
public class FahrenheitViewModel : INotifyPropertyChanged {
    private double _fahrenheitTemperature;

    public event PropertyChangedEventHandler PropertyChanged;

    public double FahrenheitTemperature
    {
        get { return _fahrenheitTemperature; }
        private set {
            if (_fahrenheitTemperature != value)
            {
                _fahrenheitTemperature = value;
                RaisePropertyChanged("FahrenheitTemperature");
            }
        }
    }

    public FahrenheitViewModel()
    {
        TemperatureService.Current.Temperatures.Subscribe(t => FahrenheitTemperature = (9 / 5) * t + 32);
    }

    private void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Et voilà c’est a peu près tout. Tout fonctionne déjà correctement ainsi. Chaque view model s’abonne à l’observable de températures définie dans la classe TemperatureService, que nous avons créée précédemment, et met à jour la température à afficher.

Conclusion

On a maintenant un service bi-directionnel utilisé par une application Silverlight utilisant le protocole http. On utilise Rx pour simplifier notre code et pour partager la même connexion entre différents view models qui nécessitent tous de recevoir la même information au même moment.

Je trouve ça plutôt sexy et vous ?

Comme d’habitude vous trouverez le code de cet article sur mon skydrive.

Updated:

Leave a Comment