Silverlight Polling Duplex WCF Service

Jan Holan       1. 11. 2011       Silverlight, WCF/WS       6076 zobrazení

V tomto článku předkládám kompletní a fungující příklad na použití duplexní komunikace s WCF službou pro Silvelight 4.0 klienty. Duplexní komunikace umožňuje scénáře, kdy je potřeba ze strany serveru zasílat zprávy (notifikace) na klienty (klient tedy může reagovat na událost vznikající na serveru).

V příkladu je velmi důležité, že se jedná o příklad fungující, protože většina existujících příkladů na Polling Duplex WCF Service pro Silverlight (i včetně těch oficiálních v MSDN) buď nefungují, nebo jsou pro starší verze Silverlight, nebo neřeší spoustu problému, které jsou pro reálné použití nutné vyřešit. Hlavní problémy jsou způsobené rozdílným chováním různých verzí assembly System.ServiceModel.PollingDuplex.dll (pro každou verzi Silverlight je jiná verze knihoven). Dále pak člověk obvykle narazí na takové příklady, které se zdají že fungují, ale po 10 minutách, což odpovídá vypršení standartního InactivityTimeout, chodit přestanou. A ještě upozornění, v článku předpokládám aspoň základní znalosti technologie WCF.

Pro použití duplexní komunikace si vytvoříme jednoduchý příklad Chat komunikátoru. Bude fungovat tak, že každý spuštěný klient bude moci odesílat textové zprávy, a každý klient obdrží všechny zprávy od všech klientů.

Vytvoření Polling Duplex WCF služby

Při vytvoření Silverlight 4.0 projektu zvolíme vytvoření i Webu, kde kromě hostování Silvelight aplikace bude také hostována naše Polling Duplex WCF služba. Aby jsme jí mohli implementovat, musíme do referencí webového projektu ještě přidat assembly System.ServiceModel.PollingDuplex.dll, která je součástí Silverlight 4 SDK a je po nainstalování umístěna v adresáři C:\Program Files (x86)\Microsoft SDKs\Silverlight\v4.0\Libraries\Server (pozor stejně jmenující se jiná assembly existuje i podadresáři client, a také je důležité vybrat správnou verzi pro Silverlight 4.0).

V projektu vytvoříme WCF službu nazvanou NotificationService.svc, její kontrakt bude vypadat následovně:

[ServiceContract(CallbackContract = typeof(INotificationClient), SessionMode = SessionMode.Required)]
public interface INotificationService
{
    [OperationContract(IsOneWay = true)]
    void RegisterClient();

    [OperationContract(IsOneWay = true)]
    void Publish(string message);
}

[ServiceContract]
public interface INotificationClient
{
    [OperationContract(IsOneWay = true, AsyncPattern = true)]
    IAsyncResult BeginUpdate(string Message, AsyncCallback callback, object state);
    void EndUpdate(IAsyncResult result);
}

Interface INotificationService bude obsahovat metodu RegisterClient pro zaregistrování přijímání zpráv a metodu Publish pro odeslání zprávy. V atributu ServiceContract pomoci parametru CallbackContract ještě zaregistrujeme interface pro callback kontrakt INotificationClient. Ten obsahuje metodu Update, která bude sloužit pro notifikaci na klientu. Protože nechceme, aby odesílání na jednoho klienta blokovalo klienty jiné, tuto metodu definujeme jako asynchronní (pomoci Begin End paternu). (Obecně ale není podmínkou mít v callback kontraktu metody pouze asynchronní).

Code behind služby NotificationService.svc bude následující:

/// <summary>
/// WCF duplexní služba pro zasílání notifikací na klienty.
/// </summary>
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
public class NotificationService : INotificationService
{
    #region member varible and default property initialization
    private static HashSet<INotificationClient> s_Clients = new HashSet<INotificationClient>();
    #endregion

    #region action methods
    public void RegisterClient()
    {
        lock (s_Clients)
        {
            //Get client callback channel
            s_Clients.Add(OperationContext.Current.GetCallbackChannel<INotificationClient>());
        }
    }

    public void Publish(string message)
    {
        Update(message);
    }
    #endregion

    #region private member functions
    private static void Update(string message)
    {
        if (message == null)
        {
            throw new ArgumentNullException("message");
        }

        List<INotificationClient> clientsToRemove = null;

        lock (s_Clients)
        {
            foreach (var client in s_Clients)
            {
                try
                {
                    //Send data to the client
                    client.BeginUpdate(message, UpdateCompleted, client);
                }
                catch (TimeoutException)
                {
                    if (clientsToRemove == null)
                    {
                        clientsToRemove = new List<INotificationClient>();
                    }
                    clientsToRemove.Add(client);
                }
                catch (CommunicationException)
                {
                    if (clientsToRemove == null)
                    {
                        clientsToRemove = new List<INotificationClient>();
                    }
                    clientsToRemove.Add(client);
                }
            }

            if (clientsToRemove != null)
            {
                foreach (var client in clientsToRemove)
                {
                    s_Clients.Remove(client);
                }
            }
        }
    }

    private static void UpdateCompleted(IAsyncResult result)
    {
        var client = ((INotificationClient)result.AsyncState);

        try
        {
            client.EndUpdate(result);
        }
        catch (CommunicationException)
        {
            lock (s_Clients)
            {
                s_Clients.Remove(client);
            }
        }
        catch (TimeoutException)
        {
            lock (s_Clients)
            {
                s_Clients.Remove(client);
            }
        }
    }
    #endregion
}

Služba si udržuje všechny aktuálně zaregistrované (běžící) klienty ve statické proměnné s_Clients typu HashSet. Ty se přidávají v metodě RegisterClient (klient se získá metodou GetCallbackChannel). Pokud klient odešle zprávu, zavolá se metoda Update, která prochází registrované klienty a provádí volání metody BeginUpdate (volá asynchroně metodu Update callback kontraktu). Pokud dojde k chybě komunikace (CommunicationException nebo TimeoutException), což může být způsobené nejčastěji tím, že klient již neběží, je z HashSetu odebrán. Stejné ošetření je i v callback proceduře UpdateCompleted pro volání EndUpdate.

K dokončení serverové strany aplikace ještě potřebujeme službu nakonfigurovat, protože je služba hostovaná na webu v IIS, provedeme to nejjednodušeji ve Web.config souboru.

<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="pollingDuplexBindingConfiguration" duplexMode="MultipleMessagesPerPoll" maxOutputDelay="00:00:01"/>
    </pollingDuplexHttpBinding>
  </bindings>

  <behaviors>
    <serviceBehaviors>
      <behavior name="">
        <serviceMetadata httpGetEnabled="true"/>
        <serviceDebug includeExceptionDetailInFaults="false"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>

  <services>
    <service name="SilverlightDuplexSample.Web.NotificationService">
      <endpoint address="" binding="pollingDuplexHttpBinding" bindingConfiguration="pollingDuplexBindingConfiguration" contract="SilverlightDuplexSample.Web.INotificationService"/>
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    </service>
  </services>
</system.serviceModel>

Jako binding použijeme pollingDuplexHttpBinding. Protože se ale nachází v přidané assembly System.ServiceModel.PollingDuplex.dll, není konfigurační element <pollingDuplexHttpBinding /> známý rovnou, musíme ho tedy v sekci extensions/bindingExtensions napřed zaregistrovat.

Při konfiguraci lze nastavit několik parametrů bindingu, atribut duplexMode je nastaven na hodnotu MultipleMessagesPerPoll, jedná se o zapnutí nového módu dostupného od Silverlight 4.0. Rozdíl je v tom, že výchozí mód SingleMessagePerPoll odesílá požadavky na klienty pouze po jednom, kdyžto mód MultipleMessagesPerPoll jich umožňuje odeslat najednou více (o tom je více popsáno zde).

Dále nastavuji parametr MaxOutputDelay, jedná se o maximální dobu, po které se po obdržení požadavku zpráva musí odeslat na klienta (parametr je pouze pro MultipleMessagesPerPoll mód). Výchozí hodnota je 200 ms, já ho zde čistě z optimalizačních důvodů prodlužuji na 1 sec. Pozor, že pokud byste vycházeli z příkladu na MSDN (jako já když jsem začínal), tam je tato hodnota nastavena až na 7 sec, se kterou jiný než ukázaný scénář nemusí fungovat správně. Poslední důležitý parametr bindingu je InactivityTimeout, ten sice nechávám na výchozí hodnotě 10 min, ale je velmi důležité si toto nastavení uvědomit (bude popsáno dále v části klienta). Více o jednotlivých timeout hodnotách duplexní komunikace je popsáno zde.

Pozn:. V configu dále nastavuji httpGetEnabled service behavior, aby se služba chovala jako standardní webová služba, to sice není k funkčnosti příkladu povinné, ale já to pro služby hostované na IIS nastavuji.

Ještě si všimněte, že na interface služby mám definováno SessionMode = SessionMode.Required, polling duplexní služba totiž vyžaduje používání WCF session (neplést s ASP.NET HttpContext.Session). Při výchozí hodnotě Allowed, by služba také chodila, ale takto je to ošetřeno přímo v její definici.

Tím máme serverovou stranu hotovou, pro zájemce ještě uvedu alternativní možnost konfigurace bindingu pomoci CustomBinding (výpis obsahuje pouze změněné části configu).

<extensions>
  ...
  <bindingElementExtensions>
    <add name="pollingDuplex" type="System.ServiceModel.Configuration.PollingDuplexElement,System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </bindingElementExtensions>
</extensions>

<bindings>
  <customBinding>
    <binding name="pollingDuplexBindingConfiguration">
      <pollingDuplex duplexMode="MultipleMessagesPerPoll" maxOutputDelay="00:00:01"/>
      <binaryMessageEncoding />
      <httpTransport transferMode="StreamedResponse" />
    </binding>
  </customBinding>
</bindings>

<endpoint address="" binding="customBinding" bindingConfiguration="pollingDuplexBindingConfiguration" contract="SilverlightDuplexSample.Web.INotificationService"/>

Tato definice bude co do funkčnosti ekvivalentní s definicí původní (dokonce lze tato nastavení pro client a server kombinovat).

Pozn.: Nastavení WCF služby můžeme kromě definování ve web.config provést také kódem. Pro svc služby k tomu potřebujeme vytvořit vlastní třídu ServiceHost a ServiceHostFactory (zděděnou z ServiceHostFactoryBase) a tu napojit v souboru svc atributem Factory. Příklad této třídy NotificationServiceHostFactory pro naší službu si můžete prohlédnout zde. Definice svc služby by pak vypadalo následovně:

<%@ ServiceHost Language="C#" Debug="true" Service="SilverlightDuplexSample.Web.NotificationService" CodeBehind="NotificationService.svc.cs" Factory="SilverlightDuplexSample.Web.NotificationServiceHostFactory" %>

Vytvoření clienta

Na straně klienta si necháme klasickým způsobem přes Add Service Reference vygenerovat třídy pro volání naší služby. Tím se zároveň automaticky do projektu přidají reference na assembly System.ServiceModel.Extensions.dll a hlavně System.ServiceModel.PollingDuplex.dll ze Silverlight SDK (z adresáře c:\Program Files (x86)\Microsoft SDKs\Silverlight\v4.0\Libraries\Client). Také si všimněte, že v souboru ServiceReferences.ClientConfig se pro duplexní službu konfigurace automaticky nevytvoří. My ale stejně provedeme nutné nastavení služby kódem.

Ten bude umístěn ve statické třídě Proxy, která bude zaobalovat volání WCF služby (které je v Silverlight asynchronní). Zdrojový kód celé třídy je k dispozici zde. Kód pro konfiguraci připojení služby je v metodě EnsureProxy a je následující:

private static NotificationServiceClient s_NotificationService;

private static bool EnsureProxy()
{
    if (s_NotificationService != null)
    {
        return false;
    }

    var serviceUri = new Uri(System.Windows.Browser.HtmlPage.Document.DocumentUri, "NotificationService.svc");

    var address = new System.ServiceModel.EndpointAddress(serviceUri);
    var binding = new System.ServiceModel.PollingDuplexHttpBinding(System.ServiceModel.Channels.PollingDuplexMode.MultipleMessagesPerPoll);
    s_NotificationService = new NotificationServiceClient(binding, address);
    s_NotificationService.UpdateReceived += new EventHandler<SilverlightDuplexSample.NotificationService.UpdateReceivedEventArgs>(NotificationService_UpdateReceived);
    return true;
}

private static void NotificationService_UpdateReceived(object sender, SilverlightDuplexSample.NotificationService.UpdateReceivedEventArgs e)
{
    ...
}

Je velice důležité, aby pokud je použit MultipleMessagesPerPoll na straně služby, byl tento mód nastaven i na klientu a opačně (takovéto chyby se pak také špatně ladí). Jinak je zde použito standardní předání adresy pro službu hostovanou na webu, který zároveň hostuje i samotnou Silverlight aplikaci. Není proto tedy potřeba URL někde konfigurovat (nebo mít uvedenou natvrdo), ale URL se odvodí podle adresy dokumentu HtmlPage.Document.DocumentUri.

Oproti normální WCF službě je zde navíc v referenci vygenerována událost <jméno client metody>Received pro každou metodu callback kontraktu. V našem případě je to tedy událost UpdateReceived. Parametry definované v metodě Update kontraktu (u nás parametr Message) máme v handleru události k dispozici přes vygenerovaný UpdateReceivedEventArgs tedy e.Message.

Při popisu služby, jsem zmiňoval problém s InactivityTimeout, o co se jedná? Pokud nebude na službu ze strany klienta po tuto dobu žádná komunikace (interní poll se nepočítá), server přestane s klientem komunikovat a přestane mu tedy zasílat jakékoliv notifikace. V našem příkladu by to tedy bylo tak, že pokud by nějaký klient neposlal 10 minut (default, který jsme ponechali nastaven na službě) žádnou zprávu (metodou Publish), přestal by přijímat zprávy od ostatních klientů. Tento problém řešíme tak, že po nějaké době kratší než InactivityTimeout např. 5 min. provedeme libovolnou komunikaci na službu (takto se to řeší i u libovolné jiné duplexní WCF služby). Pro tuto “slepou” komunikaci rovnou využijeme existující metodu RegisterClient (protože opakované volání registrace klienta nám nevadí).

Celé volání umístíme do metody InitializeNotificationService, kterou budeme volat z metody Initialize (prvotní inicializace) a z obsluhy objektu Timer s intervalem 5 min.

private static System.Threading.Timer s_NotificationServiceStayAliveTimer = new System.Threading.Timer(InitializeNotificationService, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));

public static void Initialize()
{
    InitializeNotificationService(null);
}

private static void InitializeNotificationService(object state)
{
    bool reconnected = EnsureProxy();
    var service = s_NotificationService;
    Guid token = Guid.NewGuid();
    EventHandler<System.ComponentModel.AsyncCompletedEventArgs> handler = null;
    handler = (s, e) =>
        {
            if ((Guid)e.UserState != token)
            {
                return;
            }
            service.RegisterClientCompleted -= handler;

            if (e.Error != null)
            {
                CloseProxy();
                if (!reconnected)
                {
                    InitializeNotificationService(null);
                }
            }
        };
    service.RegisterClientCompleted += handler;
    service.RegisterClientAsync(token);
}

Všimněte si zde volání metody EnsureProxy, která kromě inicializace komunikace zároveň vrací, zda došlo k novému vytvoření instance služby, pokud ne, tak je v případě chyby inicializace vyvolána ještě jednou.

Veřejný interface třídy Proxy je kromě metody Initialize tvořen dále metodou Publish a událostí UpdateReceived. Implementace metody Publish je řešena obdobně jako v metodě InitializeNotificationService.

(Pozn.: Narazil jsem i na příklad , kde byl problém s InactivityTimeout řešen na straně serveru pravidelným odesíláním “prázdné” notifikace. To sice fungovalo v Silverlight 3, kde byl odesláním notifikace InactivityTimeout resetován, ale to byla spíš chyba, kterou zmíněný příklad využíval, v Silverlight 4 toto nefunguje.)

Součástí zdrojových kódů příkladu Chat komunikátoru je ještě kód hlavní stránky aplikace, který zde již uvádět nebudu, celý příklad si můžete stáhnout zde.


Některý další příklad na řešení Polling Duplex komunikace pro Silverlight je zde (upravený příklad z MSDN ke stažení, neřeší problém InactivityTimeout a nepoužívá MultipleMessagesPerPoll mód.) a zde.

 

hodnocení článku

2 bodů / 2 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback