Spis treści:

Kategoria:C#Windows Communication Foundation


WCF i konfiguracja połączenia TCP/IP

Dlaczego TCP/IP

Jak wspominałem w którymś z poprzednich wpisów, WCF może być skonfigurowany do pracy z różnymi protokołami. Jednym z najpowszechniejszych protokołów jest właśnie protokół TCP/IP. Dlaczego mielibyśmy zmieniać HTTP na TCP/IP? Przede wszystkim dlatego, że jest szybszy. Odbywa się to jednak pewnym kosztem. HTTP to protokół tekstowy (zbudowany zresztą na bazie protokoł€ TCP/IP). HTTP jest protokołem wyższego poziomu, warstwy aplikacji, jest bardziej zrozumiały przez inne aplikacje. TCP/IP jest protokołem warstwy transportowej. Dane w TCP/IP nie są przesyłane w postaci tekstu, lecz w postaci binarnej.

Testując wydajność basicHttpBinding i netTcpBinding możemy się mocno zaskoczyć. Okazuje się, że w domyślnej konfiguracji, powtarzam, domyślnej, basicHttpBinding spisuje się przynajmniej tak samo jak netTcpBinding lub nawet lepiej. Powód tego jest dość prosty. Otóż netTcpBinding w domyślnej konfiguracji przystosowany jest do bezpiecznej transmisji danych. Wiadomo nie od dziś, że bezpieczeństwo kosztuje.

Zadaniem tego artykułu nie jest wyjaśnianie różnic między protokołami - tym zajmę się być może w innym, oddzielnym artykule. Przystąpmy zatem do części właściwej, czyli stworzenia prostej konfiguracji klient-serwer w WCF działającej w oparciu o protokół TCP/IP.

Od czego zacząć

Przypomnijmy sobie o pewnej zasadzie: pracę z WCF należy rozpocząć od zdefiniowania kontraktów, czyli intefrejsów. Sposób tworzenia i oznaczania kontraktów opisany jest tutaj: Kontrakty w WCF. Nasza aplikacja WCF będzie udostępniała usługę wyliczania składki emerytalnej z kwoty brutto pracownika zatrudnionego na podstawie umowy o pracę w roku 2011.

Stwórzmy zatem trzy projekty: projekt z interfejsami, projekt serwera implementujący interfejs i projekt klienta wykorzystujący interfejs. Referencja projektu z interfejsem musi oczywiście zostać włączona do pozostałych projektów.

Przejdźmy do samej definicji interfejsu, który będzie wyglądał następująco:

[ServiceContract] public interface ISkladki
{
    [OperationContract]
    decimal GetValue(decimal gross);
    [OperationContract]
    List<decimal> GetValues(List<decimal> grossValues);
}

Usługa będzie udostępniała dwie metody. Pierwsza policzy składki emerytalne dla pojedynczej wartości, druga natomiast składki emerytalne dla wielu wartości jednocześnie.

Dobrą praktyką jest tworzenie mocno specjalizowanych usług, które za jednym razem wykonają wiele operacji. Liczenie kilkudziesięciu wartości w jednej operacji będzie o niebo szybsze od kilkudziesięciu wywołań usługi liczącej pojedynczą wartość. Trzeba pamiętać o tym, że pod spodem znajduje się jakiś kanał transmisji, sieć. Nawiązywanie połączeń i przesyłanie danych może w szczególnych przypadkach zajmować znacznie więcej czasu niż sama operacja.

Implementacja metod kontraktu i kod serwera

Kontrakt mamy gotowy i znajduje się on w projekcie z interfejsami. Przechodzimy zatem do projektu serwera. Dodajmy sobie klasę Skladki, która będzie implementowała wspomniany interfejs. Przypominam o dodaniu referencji do projektu z interfejsami i do biblioteki System.ServiceModel. Kod klasy skladki pokazany jest poniżej:

public class Skladki : ISkladki
{
    public decimal GetValue(decimal gross)
    {
        return gross * 0.0976M;
    }

    public List<decimal> GetValues(List<decimal> grossValues)
    {
        List<decimal> returnValues = new List<decimal>();
        foreach (var value in grossValues)
            returnValues.Add(value * 0.0976M);
        return returnValues;
    }
}

Po stronie serwera należy jeszcze wystawić zaimplementowany interfejs. Sam kod klasy Program.cs nie będzie się różnił od kodu zaprezentowanego w artykule Konfiguracja WCF za pomocą plików app.config. Przypomnijmy sobie zatem, jak on wyglądał:

static void Main(string[] args)
{
  ServiceHost host = new ServiceHost(typeof(HelloWorld));
  host.Open();
  Console.WriteLine("Serwer uruchomiony");
  Console.ReadLine();
}

Strona serwerowa jest prawie gotowa. Potrzebny nam jest jeszcze plik App.config z właściwą konfiguracją. Tutaj znajduje się to, co sprawia, że nasza usługa sieciowa będzie działała w ramach TCP/IP. Zanim przystąpię do wyjaśnień, przyjrzyjmy się przykładowej konfiguracji zaprezentowanej 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"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

Zwróćmy teraz uwagę na drobne różnice. Pomijam oczywiście różnice spowodowane inną nazwą klasy i inną nazwą interfejsu. Pierwsza znacząca różnica ujawnia się w definicji adresu. W przypadku basicHttpBinding adres rozpoczynał się od http://, w przypadku TCP/IP adres należy rozpocząć od net.tcp://.

Pozostawienie w adresie http:// spowoduje błąd o następującej treści: Dostarczony schemat identyfikatora URI "http" jest nieprawidłowy; oczekiwano schematu "net.tcp". W anglojęzycznej wersji będzie to komunikat zbliżony do tego: Dostarczony schemat identyfikatora URI {0} jest nieprawidłowy; oczekiwano schematu {1}.

Druga kluczowa różnica to zmiana w atrybucie binding. Zamiast basicHttpBinding pojawia się netTcpBinding. Jest to domyślna konfiguracja sposobu przesyłania danych za pomocą protokołu TCP/IP. Jak zmieniać konfiguracje opiszę wkrótce. Dobra wiadomość jest taka, że mamy ukończony kod serwera. Można się o tym przekonać kompilując i uruchamiając aplikację.

Kod klienta

Kodu klienta nie będę szczegółowo omawiał, bo nie ma tutaj niczego specjalnego. Ot po prostu zwykły kod. Z elementami charakterystycznymi dla WCF mieliśmy już do czynienia przy okazji omawiania wcześniejszych przykładów. Pozostała część ma z WCF niewiele wspólnego. Kod klienta pokazany jest poniżej:

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();
    }
}

Zmiany na kliencie dotyczą głównie pliku konfiguracyjnego. Spośród różnych możliwości konfiguracji przedstawię chyba najprostszą z możliwych. Na skomplikowane przyjdzie jeszcze czas. Plik App.config będzie miał tym razem taką oto zawartość:

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

Różnica w stosunku do basicHttpBinding jest niewielka. Oprócz nazwy usługi i nazwy kontraktu, plik konfiguracyjny różni się sposobem podawania adresu. Tak jak po stronie serwera, tak i tutaj adres rozpoczyna się od net.tcp://.

Wnioski

Skorzystanie z protokołu TCP/IP w WCF nie jest szczególnie trudne. W najprostszej postaci sprowadza się do modyfikacji atrybutu binding w plikach konfiguracyjnych, oraz niewielkiej zmianie adresu. Podobnie jak zawsze, zachęcam do różnych eksperymentów. Ćwiczenia mogą polegać na rozszerzeniu interfejsu o dodatkowe metody liczące oprócz składki emerytalnej składkę rentową, zdrowotną. Dane mogą być zwracane w klasie, którą, przypominam, należy oznaczyć atrybutami DataContract i DataMember. Zachęcam do dzielenia się problemami i spostrzeżeniami w komentarzach.

Kategoria:C#Windows Communication Foundation

, 2013-12-20

Komentarze:

Morfi (2012-11-07 16:06:13)
Mam pytanie odnośnie serializacji interfejsów.
Czy jest to możliwe?
Przykład:
public class Nosnik
{
  List<IDane> lista = new List<IDane>();
  public Nosnik()
  {
    lista.Add(new Dane1());
    lista.Add(new Dane2());
  }
}
public interface IDane
{
  string dana { get; }
}
public class Dane1:IDane
{
  private string _dana = "Jestem obiektem klasy Dane1";
  public string dana
  {
    getreturn _dana; }
  }
}
public class Dane2:IDane
{
  private string _dana = "Jestem obiektem klasy Dane2";
  public string dana
  {
    getreturn _dana; }
  }
}
Czy taki twór można Serializować? Zgodnie z założeniem iż lista w obiekcie klasy Nosnik zawierała(przeniosła) obiekty dziedziczące po interfejsie.
Pozdrawiam
Morfi
PD (2012-11-09 21:10:41)
Da się coś takiego osiągnąć. Zagadnienie zostało przeze mnie opisane tutaj: Dziedziczenie w WCF
Problem może się pojawić jeszcze wtedy, gdy właściwość nie ma atrybutu set (ma tylko get). Wewnętrzna budowa serializatora nie pozwala na takie konstrukcje i wyświetla komunikat Brak metody Set dla właściwości XXX w typie YYY (ang. No set method for property XXX in type YYY). Da się to trochę zmiękczyć stosując kwalifikator protected, dzięki czemu tylko klasy dziedziczące będą miały dostęp (dla innych tak jak private). Serializator, co interesujce, poradzi sobie z takim czymś.
Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?