Spis treści:

Kategoria:C#Windows Communication Foundation


Udostępnianie usług WCF różnym klientom

BasicHttpBinding, NetTcpBinding, a może oba?

Różni klienci mogą mieć różne potrzeby. Przypuśćmy, że projektujemy pewną usługę WCF, która może być dostępna dla klientów wewnętrznych, działających w lokalnej sieci, oraz dla klientów zewnętrznych, korzystających z usług na całym świecie. Chcemy, aby klienci wewnętrzni korzystali z protokołu TCP/IP, bez szyfrowania, bo to zapewnia optymalną prędkość. Klienci zewnętrzni preferują transmisję tekstową przy pomocy protokołu HTTP, który jest może i nieco wolniejszy, ale jest lepiej dostępny, nie jest blokowany przez zapory ogniowe (ang. firewalle). Czy musimy wtedy tworzyć dwie różne usługi? To oczywiście pytanie retoryczne. Od razu uspokajam: nie trzeba pisać dwóch różnych implementacji. Wystarczy, że uruchomimy nasłuch na dwóch różnie konfigurowanych końcówkach. W artykule skorzystamy z serwera i klienta napisanego w artykule WCF i konfiguracja połączenia TCP/IP. Jak ustanowić i uruchomić dwie końcówki? Przyjrzyjmy się następującemu plikowi konfiguracyjnemu:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Skladki">
        <endpoint address="net.tcp://localhost:2222/Skladki"
                  binding="netTcpBinding"
                  contract="ISkladki"/>
        <endpoint address="http://localhost:2222/Skladki"
                  binding="basicHttpBinding"
                  contract="ISkladki"/>     
      </service>
    </services>
  </system.serviceModel>
</configuration>

Spróbujmy teraz uruchomić aplikację serwera. Jak można się przekonać, pojawi się błąd następującej treści: Protokół HTTP nie może zarejestrować adresu URL http://+:2222/Skladki/, ponieważ port TCP 2222 jest używany przez inną aplikację. Angielskojęzyczni użytkownicy otrzymają taki komunikat: HTTP could not register URL {0} because TCP port {1} is being used by another application. Dlaczego tak się stało? Serwer uruchamiając pierwszą końcówkę zajmuje port 2222 i spodziewa się, że pod ten adres (port należy traktować jako część adresu) będą spływały komunikaty (żądania) w formacie HTTP. Mają one nieco inną postać niż komunikaty TCP/IP, więc usługa mogłaby ich nie zrozumieć. Nie da się więc odbierać pod tym samym adresem dwóch różnych typów komunikatów.

Warto wiedzieć, że WCF może działać nie tylko na czystym HTTP i TCP/IP. Do wyboru mamy bardziej finezyjne protokoły, między innymi potoki nazwane, MSMQ, HTTPS. TCP/IP może być szyfrowane, w komunikacji możemy posługiwać sie certyfikatami. Każde takie udziwnienie najczęściej wymaga uruchomienia oddzielnej końcówki.

Niektórzy mogą zapytać, dlaczego w komunikacie o błędzie informawani jesteśmy o zajętości portu TCP, skoro uruchamiamy HTTP? Odpowiedź jest prosta: protokół HTTP jest pewnego rodzaju nadbudówką TCP/IP, inaczej mówiąc, działa w oparciu o protokół TCP/IP. Pod spodem HTTP działa stare, dobre TCP/IP.

Wprowadźmy niewielką zmianę do pliku konfiguracyjnego, a mianowicie zmieńmy numer portu. Nowy plik App.config pokazany jest poniżej:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Skladki">
        <endpoint address="net.tcp://localhost:2222/Skladki"
                  binding="netTcpBinding"
                  contract="ISkladki"/>
        <endpoint address="http://localhost:2223/Skladki"
                  binding="basicHttpBinding"
                  contract="ISkladki"/>      
      </service>
    </services>
  </system.serviceModel>
</configuration>

Tym razem serwer uruchamia się poprawnie. Na co należy zwrócić uwagę? Po pierwsze, na różnicę w adresie. Prefiks adresu określa sposób komunikacji http:// to HTTP, net.tcp:// to TCP/IP. Druga różnica to numer portu, który w przypadku HTTP został zmieniony na 2223.

Nieco inny rodzaj adresu podaje się w przypadku kolejek komunikatów (MSMQ). Atrybut binding w pliku konfiguracyjnym ustawiany jest najczęściej na netMsmqBinding, natomiast sam adres przybiera postać net.msmq://adres_komputera/rodzaj_kolejki/Nazwa_usługi. Jeszcze inny prefiks wstawiamy wtedy, gdy korzystamy z potoków nazwanych. W takim przypadku będzie to net.pipe://. Znanym prefiksem jest też https://. Sposób korzystania z tych protokołów opiszę innym razem.

Kolejne końcówki można dodawać w nieskończoność (oczywiście wszystko w ramach rozsądku). Przyjrzyjmy się jeszcze jednej konfiguracji:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Skladki">
        <endpoint address="net.tcp://localhost:2222/Skladki"
                  binding="netTcpBinding"
                  contract="ISkladki"/>
        <endpoint address="http://localhost:2223/Skladki"
                  binding="basicHttpBinding"
                  contract="ISkladki"/>
        <endpoint address="http://localhost:2224/Skladki"
                  binding="wsHttpBinding"
                  contract="ISkladki"/>       
      </service>
    </services>
  </system.serviceModel>
</configuration>

Myślę, że nie wymaga to szerszego komentarza.

Korzystanie z wielu końcówek na kliencie

Mamy wiele różnych końcówek i jedną aplikację klienta. Jak wskazać, z której akurat chcemy skorzystać? Przyjrzyjmy się, jak zbudowana jest funkcja Main aplikacji, która orzysta z jednej końcówki:

static void Main(string[] args)
{
    using (var c = new ChannelFactory<ISkladki>(""))
    {
        var s = c.CreateChannel();
        Console.WriteLine(s.GetValue(2000.0M));
        List<decimal> valuesList = new List<decimal>(){ 
            2000.0M, 2200.0M, 2400.0M,
            2600.0M, 2800.0M, 3000.0M
        };
        List<decimal> returnValues = s.GetValues(valuesList);
        foreach (var value in returnValues)
            Console.WriteLine(value);
        Console.ReadLine();
    }
}

Zmodyfikujmy także plik App.config klienta, aby mógł on korzystać z wszystkich trzech zdefiniowanych wcześniej końcówek:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint address="net.tcp://localhost:2222/Skladki"
                binding="netTcpBinding"
                contract="ISkladki"/>
      <endpoint address="http://localhost:2223/Skladki"
                binding="basicHttpBinding"
                contract="ISkladki"/>
      <endpoint address="http://localhost:2224/Skladki"
                binding="wsHttpBinding"
                contract="ISkladki"/>
    </client>
  </system.serviceModel>
</configuration>

Zanim przystąpimy do zmian spróbujmy uruchomić program. Pojawi się komunikat o następującej treści: Element podrzędny o nazwie 'endpoint' z tym samym kluczem już istnieje w tym samym zakresie konfiguracji. Elementy kolekcji muszą być unikatowe w ramach tego samego zakresu konfiguracji (np. tego samego pliku application.config). Zduplikowana wartość klucza: 'contractType:ISkladki;name:'. Jak zwykle podam również komunikat angielski: A child element named {0} with same key already exists at the same configuration scope. Collection elements must be unique within the same configuration scope (e.g. the same application.config file). Duplicate key value: {1}.

Powód takiego błędu nie jest na pierwszy rzut oka oczywisty. Aby nakierować na właściwe rozwiązanie problemu zwrócę wzrok na sposób tworzenia ChannelFactory. Teraz zadajmy sobie pytanie: skąd WCF ma wiedzieć, z której konfiguracji skorzystać? Jeżeli zobaczymy, jak nazywa się pierwszy parametr konstruktora, wszystko się zacznie rozjaśniać. Parametr nazywa się endpointConfigurationName. Do tej pory pozostawał on pusty, ale nadszedł czas, aby z niego skorzystać. Teraz drugie pytanie: jak WCF ma znaleźć końcówkę po nazwie, jeżeli końcówki nie mają nazw? To pytanie jest kluczowe. Każda końcówka w kolekcji może mieć swoją nazwę i w tym kierunku pójdziemy. Do każdej z końcówek w pliku App.config dodamy atrybut name. Gotowy plik App.config nowego klienta zaprezentowany jest poniżej:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="Tcp"
                address="net.tcp://localhost:2222/Skladki"
                binding="netTcpBinding"
                contract="ISkladki"/>
      <endpoint name="Http"
                address="http://localhost:2223/Skladki"
                binding="basicHttpBinding"
                contract="ISkladki"/>
      <endpoint name="Ws"
                address="http://localhost:2224/Skladki"
                binding="wsHttpBinding"
                contract="ISkladki"/>
    </client>
  </system.serviceModel>
</configuration>

Teraz już tylko pozostaje nam wprowadzenie zmian w kodzie klienta. Gotowy kod klienta pokazany jest poniżej:

static void Main(string[] args)
{
    using (var c = new ChannelFactory<ISkladki>("Tcp"))
    {
        var s = c.CreateChannel();
        Console.WriteLine(s.GetValue(2000.0M));
        List<decimal> valuesList = new List<decimal>(){ 
            2000.0M, 2200.0M, 2400.0M,
            2600.0M, 2800.0M, 3000.0M
        };
        List<decimal> returnValues = s.GetValues(valuesList);
        foreach (var value in returnValues)
            Console.WriteLine(value);
        Console.ReadLine();
    }
}

Wspomniałem już wcześniej o pierwszym parametrze konstruktora klasy ChannelFactory. Zamiast pustego łańcucha znaków pojawiła się tam nazwa konkretnej konfiguracji, w tym przypadku Tcp. O tym, że aplikacja zaczęła działać, można się przekonać kompilując i uruchamiając projekt. Należy pamiętać o wcześniejszym uruchomieniu aplikacji serwera.

Jako ćwiczenie polecam przeprowadzenie testów wydajnościowych na różnych typach końcówek. Wyniki należy traktować tylko jako ciekawostkę, nie jako wyznacznik rzeczywistej prędkości różnych protokołów. Pamiętajmy, że są to konfiguracje domyślne i mają różne możliwości (np. netTcpBinding domyślnie zabezpiecza komunikat w warstwie transportu, basicHttpBinding przesyłany jest w czystej postaci tekstowej). Zachęcam także do dzielenia się przemyśleniami w komentarzach.

Kategoria:C#Windows Communication Foundation

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?