Spis treści:

Kategoria:C#Windows Communication Foundation


Komunikacja dwukierunkowa w WCF (Duplex)

Nie tylko jako usługa

Do tej pory wszystkie przykłady komunikacji WCF bazowały na tradycyjnym podejściu usługowym. W takim układzie klient pyta serwer i czeka na odpowiedź. Serwer jest bierny i sam nie może nic zrobić, dopóki klient sobie czegoś nie zażyczy. A gdyby tak serwer chciał poinformować klienta o zajściu jakiegoś zdarzenia? Pierwsze, i jak się okazuje, nienajgorsze rozwiązanie polega na... uruchomieniu usługi przez klienta! Informujemy serwer o adresie, pod którym zamierzamy nasłuchiwać i voilà! Serwer może się komunikować z klientem. Rozwiązanie jest niezwykle skuteczne, w minimalnym stopniu angażuje zasoby serwera, który takich klientów może mieć dużo. Problem w tym, że klient musi wystawić końcówkę serwerowi, co może być problemem gdy taka końcówka skrywa się za zaporami sieciowymi (ang. firewall).

Drugim sposobem jest okresowe przepytywanie serwera. Co, powiedzmy, 10 sekund wysyłamy do serwera pytanie: masz coś dla nas? Jeżeli serwer ma, to odsyła. Jeżeli nie, to nie. Problem w tym, że kilkanaście aplikacji klienckich może tak zbombardować serwer, że nie będzie on miał czasu na żadne inne operacje. Takie jałowe odpowiadanie też kosztuje.

Trzecim rozwiązaniem, którym chciałbym się dokładniej zająć, jest komunikacja dwukierunkowa. W takim przypadku klient utrzymuje połączenie z serwerem, przez co serwer może w dowolnej chwili przesłać zdefiniowany wcześniej kontraktem komunikatTrzeba wiedzieć, że nie wszystkie metody komunikacji pozwalają na utrzymywanie połączenia. HTTP jest protokołem bezpołączeniowym, więc z definicji połączenia utrzymywać nie może. Potrzeba komunikacji dwukierunkowej HTTP z jednoczesną potrzebą utrzymania w miarę spójnego API doprowadziły do wiązania WSDualHttpBinding. Takie połączenie HTTP, z wiązaniem WSDualHttpBinding, jest tak naprawdę... dwoma połączeniami HTTP. Jednym od klienta do serwera, a drugim od serwera do klienta. Jest to tylko implementacyjny trik, który ukrywa szczegóły pierwszej, opisanej we wstępie, metody dwustronnej komunikacji. Przejmuje też wszystkie wady tej pierwszej metody. TCP pozwala na realizację prawdziwego połączenia dwukierunkowego, bo z definicji jest połączeniem dwukierunkowym.. Przejdźmy zatem do szczegółów.

Jak to działa

Wszystkie poprzednie projekty WCF rozpoczynały się od projektu interfejsu (zobacz wpisy w kategorii Windows Communication Foundation (10)). Definiowaliśmy interfejs (kontrakt), który musiał być zaimplementowany przez serwer. Tym razem w wymianie uczestniczą dwie strony - każda z nich ma swój kontrakt. Kontrakt ma serwer i kontrakt ma klient. O ile istnieje kanał komunikacyjny (klient nie zamknął kanału, serwer pamięta kanał z interfejsem zwrotnym), serwer może wysyłać komunikaty do klienta. To w zasadzie wszystko. Cała reszta jest już szczegółem implementacyjnym. Różnic jest kilka. Po pierwsze, wcześniej do tworzenia kanału komunikacyjnego korzystaliśmy z klasy ChannelFactory<T>, teraz korzystamy z klasy DuplexChannelFactory<T>. Po drugie, coś na kliencie musi nasłuchiwać - stąd dodatkowy parametr klasy DuplexChannelFactory<T>. Po trzecie, na serwerze skorzystamy z klasy OperationContext do pobrania kanału zwrotnego. To są rzeczy na które należy zwrocić uwagę - wszystkie inne powinny być znajome. Przejdźmy zatem do przykładu.

Dwaj odbiorcy, dwa kontrakty

Pokazany przykład pokazuje, w jaki sposób można zrealizować powiadamianie klienta o zakończeniu długotrwałej operacji. Przyjrzyjmy się przykładowym kontraktom zaprezentowanym poniżej:

using System.ServiceModel;

//Kontrakt serwera
[ServiceContract(CallbackContract = typeof(INotify))]
public interface IRegister
{
    [OperationContract(IsOneWay = true)]
    void RunTask();
}
using System.ServiceModel;

//Kontrakt klienta
[ServiceContract]
public interface INotify
{
    [OperationContract(IsOneWay = true)]
    void Notify();
}

Warto zwrócić uwagę, że w kontrakcie serwera definiujemy również kontrakt zwrotny obsługiwany przez klienta. Serwer musi mieć pewność, że moze taki kontrakt zwrotny uzyskać. Druga rzecz, o której jeszcze nie było okazji powiedzieć, jest atrybut IsOneWay. Jest on obecny w kontrakcie metod serwera i klienta, a oznacza, że strona wywołująca nie czeka na odpowiedź. Jest to komunikat typu: wyślij i zapomnij. Dzięki takiemu rozwiązaniu klient może wykonywać inne operacje podczas przetwarzania zadań przez serwer. Podobnie serwer - nie musi czekać na odpowiedź klienta. Zyskujemy dużo, a ile tracimy? Tracimy informację o błędach w dostarczeniu komunikatów (jako test można sobie wyrzucić wyjątek w metodzie implementującej interfejs serwera i w metodzie implementującej interfejs klienta). Niezawodność w dostarczaniu komunikatów może być bardzo ważna (transakcje finansowe) lub mało istotna (bieżąca temperatura w procesie wolnozmiennym).

Implementacja serwera

Przykład implementacji serwera pokazany jest poniżej:

using System.ServiceModel;
using System.Threading;

public class RegisterIRegister
{
    public void RunTask()
    {
        //Zapamiętaj kanał zwrotny
        INotify CallbackChannel = OperationContext.Current.GetCallbackChannel<INotify>();
        //Symulujemy jakieś długie obliczenia
        Thread.Sleep(1000);
        //Powiadamiamy klienta o zakończeniu
        CallbackChannel.Notify();
    }
}

Każde wywołanie WCF działa w jakimś kontekście. Nie zawsze trzeba do niego sięgać, ale czasami przenoszone przez niego informacje są niezbędne do realizacji określonych zadań. W kontekście znajdują się między innymi informacje o użytkowniku, który wykonuje operację (jeżeli odpowiednio skonfigurujemy usługę), ale także informacja o kanale zwrotnym (jeżeli takowa jest). Służy do tego metoda GetCallbackChannel<T>(). Kontekst przechowywany jest w obszarze zwanym TLSTo ta sama lokalna pamięć wątków, która była dostępna od pierszych wersji Windows. Zaskakujące jest to, że, pomimo już słusznego wieku, wiele osób nawet nie wie o ich istnieniu. W .NET istnieje cały zestaw funkcji do obsługi lokalnej pamięci wątku w klasie System.Threading.Thread o nazwie *DataSlot, istnieje upraszczający wszystko atrybut ThreadStatic, a w .NET 4.0 typ ogólny System.Threading.ThreadLocal<T>. (ang. Thread Local Storage - lokalna pamięć wątku) i jest dostępny tylko w tym wątku, który wykonuje daną operację. Jeżeli po stronie serwera uruchamiamy jakiś dodatkowy wątek, należy pamiętać o wcześniejszym zapamiętaniu i przekazaniu informacji z kontekstu. W nowym wątku nie będą dostępne.

Usługa i konfiguracja serwera

Sam serwer, w sensie gospodarza usługi, i plik konfiguracyjny nie zawierają żadnych nowych elementów. Aby przykład był kompletny, zamieszczę je do wglądu:

static void Main(string[] args)
{
    ServiceHost host = new ServiceHost(typeof(Register));
    host.Open();
    Console.WriteLine("Opened...");
    Console.ReadLine();
}
<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Register">
        <endpoint address="net.tcp://localhost:2222/Register"
                binding="netTcpBinding"
                contract="IRegister">
        </endpoint>
      </service>
    </services>
  </system.serviceModel>
</configuration>

Jedyną rzeczą wartą omówienia są wiązania - nie wszystkie wiązania obsługują komunikację dwukierunkową. W zasadzie to obsługują ją tylko dwa - WsDualHttpBinding i TcpBinding. To należy zapamiętać.

Nasłuchiwanie po stronie klienta

Przeanalizujmy całą sytuację - serwer odbiera komunikat, uruchamia zadanie, a następnie, po zakończeniu przetwarzania, próbuje powiadomić klienta. Gdzieś na kliencie musi istnieć coś, jakaś klasa, która obsłuży obiecaną w kontrakcie metodę. Do tego posłuży nam klasa Listener. Przykład takiej implementacji został pokazany poniżej:

using System;
using System.Threading;

//Klasa implementująca interfejs nasłuchowy
public class ListenerINotify
{
    //Obiekt synchronizacji, ustawiany po otrzymaniu odpowiedzi z serwera
    private AutoResetEvent notifyEvent = new AutoResetEvent(false);
    public AutoResetEvent FinishedEvent
    {
        get
        {
            return notifyEvent;
        }
    }

    public void Notify()
    {
        Console.WriteLine("Notified...");
        notifyEvent.Set();
    }
}

Dla kogoś znającego techniki programowania wielowątkowego klasa wyda się dość czytelna (tak, przebieg programu nie zostanie naruszony, komunikat przyjdzie w nowym wątku!). Po odebraniu komunikatu ustawiamy zmienną reprezentującą zdarzenieZdarzenie jest obiektem synchronizacji obsługiwanym przez system operacyjny. Oczekiwanie na takie zdarzenie zajmuje minimalne zasoby i realizowane jest na poziomie przydziału procesora. Jeżeli wątek czeka na zdarzenie, które nie zostało ustawione, wątek przechodzi w stan czekania i jest pomijany przez mechanizmy przydzielające czas procesora. Dopiero ustawienie zdarzenia (lub opcjonalne ustawienie limitu czasowego) może ten wątek obudzić. Zdarzenia, podobnie jak lokalna pamięć wątku, istnieją w systemie operacyjnym Windows od dawna (zob. Windows 95 i wielowątkowość z wywłaszczaniem)., zapamiętujemy ewentualne wyniki obliczeń i w zasadzie możemy na tym poprzestać. Reszta zależy od struktury programu i od tego, kto potrzebuje danych zwrotnych. W naszym przypadku głównym zainteresowanym jest metoda Main klienta. Ta właśnie metoda Main może wykonać swoje operacje i poczekać na zaistnienie zdarzenia. Nie jest to jedyne rozwiązanie - można powiadomić klasę jakimś zdarzeniem, skorzystać z kolejki komunikatów czy zastosować jeszcze inną, zależną od systemu metodę.

Aplikacja klienta

Mamy kontrakty, mamy serwer, mamy klasę do nasłuchu - czas na ostatni klocek. Przyjrzyjmy się implementacji klienta pokazanej na poniższym listingu:

static void Main(string[] args)
{
    //Klasa nasłuchowa
    var listener = new Listener();

    //Tworzymy fabrykę kanałów i sam kanał
    using (var factory = new DuplexChannelFactory<IRegister>(listener, ""))
    using (var baseChannel = (IClientChannel)factory.CreateChannel())
    {
        IRegister channel = (IRegister)baseChannel;
        //Uruchamiamy zadanie na serwerze,
        //zostaniemy poinformowani o zakończeniu
        channel.RunTask();

        //Wykonujemy inne operacje
        Console.WriteLine("Do other stuff...");

        //Czekamy na odpowiedź
        listener.FinishedEvent.WaitOne();
        //Otrzmyalimy odpowiedź, możemy spokojnie zakończyć pracę
        Console.WriteLine("Exit...");
    }
    Console.ReadLine();
}

Jak wcześniej wspomniałem, tym razem przy tworzeniu kanału korzystamy z klasy DuplexChannelFactory<T>, przekazując klasę nasłuchową. Potem, po utworzeniu kanału, uruchamiamy zadanie, wykonujemy inne operacje i czekamy na zaistnienie zdarzenia. Gdy zdarzenie nastąpi, możemy spokojne zamknąć kanał (zadziała using). Plik konfiguracyjny klienta nie zawiera żadnych dodatkowych kontrukcji:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint address="net.tcp://localhost:2222/Register"
                binding="netTcpBinding"
                contract="IRegister">
      </endpoint>
    </client>
  </system.serviceModel>
</configuration>

Po uruchomieniu aplikacji klienckiej i odczekaniu sekundy z drobnymi otrzymamy taki oto rezultat:

Do other stuff...
Notified...
Exit...

Komunikacja dwukierunkowa ma znacznie większe możliwości. Pokazany powyżej przykład to tylko jedno z zastosowań. Nie ma żadnych przeciwwskazań, aby stworzyć prosty czat, bardziej zaawansowany komunikator, grę sieciową czy też moduł powiadamiania o aktywności użytkowników. Wszystko zależy od potrzeby, wyobraźni i chęci.

Kategoria:C#Windows Communication Foundation

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
bardo ciekawe , można dzięki takim wpisom dostrzec wędkę..
Bardzo dziękuję za jasne tłumaczenie z dobrze dobranym przykładem!
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !