Spis treści:

Kategoria:C#Windows Communication FoundationSerializacja


Serializacja w WCF od środka

Zamiana obiektów na strumień bajtów

Na początku postawię proste pytanie: czym zajmuje się WCF? Jak sama nazwa wskazuje - komunikacją. A czym jest ta komunikacja? Powołując się na słownik języka polskiego PWN: przepływ informacji między urządzeniami, np. telefonami lub komputerami. Aby komunikacja mogła zaistnieć obie strony muszą się rozumieć. Załóżmy, że jeden komputer ma obiekt w swojej pamięci i chce go przesłać do komputera drugiego. Jak on to robi? Wiemy, że dane w komputerze to zera i jedynki, te zera i jedynki układają się w bajty. Każdy zatem obiekt da się w ten sposób wyrazić. Języki wysokiego poziomu ukrywają przed nami implementacje typów i ich wewnętrzną, zero-jedynkową postać. Czasami jednak zachodzi potrzeba pobrania tych fizycznych bajtów. Taki proces nazywamy serializacją. Zgodnie z jedną z definicji serializacja to proces przekształcania obiektów, tj. instancji określonych klas, do postaci szeregowej, czyli w strumień bajtów, z zachowaniem aktualnego stanu obiektu.

Strumień bajtów ma jednak tę właściwość, że może być różnie interpretowany. Różne pary zer i jedynek mogą oznaczać kombinacje TAK-NIE, BIAŁY-CZARNY, WŁĄCZONY-WYŁĄCZONY. Cztery położone obok siebie bajty to liczba całkowita, która może reprezentować liczbę osób w zespole, wiek uczestnika. Druga strona tego nie wie, dopóki nie powiemy mu jak ma odebrany strumień interpretować - do tego służą kontrakty (zobacz też Kontrakty w WCF).

Problem przesyłania danych i ich interpretacji został zauważony już dawno. Jedną z odpowiedzi był XML - format bardziej czytelny dla człowieka. Składa się on ze znaczników, które w jakiś sposób podpowiadają znaczenie poszczególnych wartości. Odbywa się to jednak kosztem dodatkowych bajtów potrzebnych do przesłania obiektu. Tyle teorii, teraz przykłady.

Z jakiej serializacji korzysta WCF?

WCF może korzystać z różnych klas serializujących (może między innymi serializować do postaci JSON). Domyslną klasą serializującą jest DataContractSerializer, która zamienia obiekty na postać XML. Popatrzmy na fragment kodu:

//Definicja klasy
public class Person
{
    public string FirstName { getset; }
    public string LastName { getset; }
    public int Age { getset; }
}

//Gdzieś w kodzie...
Person person = new Person()
{
    FirstName = "Gordon",
    LastName = "Hottentot",
    Age = 30
};
DataContractSerializer serializer = new DataContractSerializer(typeof(Person));
MemoryStream outputStream = new MemoryStream();

//Serializacja
serializer.WriteObject(outputStream, person);

//Pobierz dane ze strumienia
string serializedValue = Encoding.UTF8.GetString(outputStream.ToArray());
outputStream.Position = 0;

//Deserializacja
var deserializedPerson = serializer.ReadObject(outputStream);

Dane, które będą przesyłane przez WFC, konwertowane są do postaci strumienia bajtów. Ten strumień bajtów jest jednak niczym innym jak tylko zakodowanym łańcuchem UTF8. Jego wartość pobrana do zmiennej serializedValue wygląda następująco:

<Person xmlns="http://schemas.datacontract.org/2004/07/"
        xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Age>30</Age>
  <FirstName>Gordon</FirstName>
  <LastName>Hottentot</LastName>
</Person>

Taki właśnie komunikat zostanie odebrany po drugiej stronie kanału komunikacyjnego.

Kontrola nad przesyłanymi danymi

WCF daje nam sporą kontrolę nad samym wyglądem komunikatu. Po pierwsze, można usunąć przestrzeń nazw xmlns="http://schemas.datacontract.org/2004/07/" definiując klasę następująco:

[DataContract(Namespace="")]
public class Person
{
    [DataMember]
    public string FirstName { getset; }

    [DataMember]
    public string LastName { getset; }

    [DataMember]
    public int Age { getset; }
}

Komunikat WCF będzie nieco krótszy:

<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Age>30</Age>
  <FirstName>Gordon</FirstName>
  <LastName>Hottentot</LastName>
</Person>

Częstym przypadkiem jest tworzenie aplikacji, w której klient i serwer korzystają ze wspólnych interfejsów. Czy wtedy potrzebny jest nam tak pięknie wyglądający XML? Można wykorzystać tę furtkę do znacznego zmniejszenia rozmiaru przesyłanego kumunikatu definiując nazwy dla każdego znacznika z osobna:

[DataContract(Namespace="", Name="PRS")]
public class Person
{
    [DataMember(Name="FN")]
    public string FirstName { getset; }

    [DataMember(Name="LN")]
    public string LastName { getset; }

    [DataMember]
    public int Age { getset; }
}

Wynik serializacji będzie następujący:

<PRS xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Age>30</Age>
  <FN>Gordon</FN>
  <LN>Hottentot</LN>
</PRS>

Pokazany przykład nie jest może reprezentatywny, bo zwykle nie optymalizuje się przesyłania tak małego obiektu, ale już tutaj widać potencjał. Pierwsza wersja obiektu serializowała się na 191 bajtach, natomiast pokazana przed chwilą potrzebowała już tylko 110 bajtów. Warto wiedzieć, że elementy XML przyjmują domyślnie takie same nazwy jak serializowane pola lub właściwości. Jeżeli mamy zatem wartość typu int z długą nazwą, wtedy znacznik otwierający i zamykający będzie zajmował znacznie więcej miejsca niż sama wartość. Dla jednego obiektu nie ma to znaczenia, ale jeżeli serializujemy listę kilku tysięcy wartości...

Zaraz, zaraz! A co z listami?

Serializacja list i tablic

Przyjrzyjmy się najpierw sposobowi serializacji list:

Person person1 = new Person()
{
    FirstName = "Gordon",
    LastName = "Hottentot",
    Age = 30
};
Person person2 = new Person()
{
    FirstName = "Paul",
    LastName = "Stealhanded",
    Age = 28
};
List<Person> list = new List<Person>() { person1, person2 };

DataContractSerializer serializer = new DataContractSerializer(typeof(List<Person>));
MemoryStream outputStream = new MemoryStream();
serializer.WriteObject(outputStream, list);
string serializedValue = Encoding.UTF8.GetString(outputStream.ToArray());
outputStream.Position = 0;
var deserializedPerson = serializer.ReadObject(outputStream);

Oraz wynikowi serializacji:

<ArrayOfPRS xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <PRS><Age>30</Age><FN>Gordon</FN><LN>Hottentot</LN></PRS>
  <PRS><Age>28</Age><FN>Paul</FN><LN>Stealhanded</LN></PRS>
</ArrayOfPRS>

Co się stanie, gdy zamiast listy przekażemy tablicę? Okazuje się, że nic się nie zmieni. Rezultat będzie identyczny! Już sama nazwa węzła głównego podpowiada nam, że mamy do czynienia z tablicą (ArrayOf) i tak rzeczywiście jest.

Skąd taka zbieżność? Wiemy, że w .NET istnieje bardzo dużo różnych struktur reprezetujących kolekcje i każda z tych kolekcji musiałaby być w jakiś sposób oznaczana w generowanym komunikacje XML. Popatrzmy jednak na to od strony logicznej - jakie jest zadanie listy, tablicy i innych kolekcji? Ich zadaniem jest przechowywanie wielu elementów. Różnica jest w sposobie zarządzania nimi. Tablica ma zdefiniowany rozmiar, listę można rozszerzać. Jeżeli jednak mamy do czynienia z transmisją danych, wtedy wszystkie te dodatki do kolekcji nie są nam potrzebne. Nie potrzebujemy funkcjonalności, tylko danych! Dostęp do nich zapewnia nam już sama implementacja interfejsu IList i tutaj należy szukać rozwiązania zagadki. Zapamiętajmy: wszystkie struktury implementujące IList będą serializowane tak samo.

Serializacja słowników

Drugim szczególnym rodzajem kolekcji, po kolekcjach typu IList są słowniki. Serializacja jest bardzo podobna - trochę magicznie jest po serializacji. Przyjrzyjmy się prostemu słownikowi:

Dictionary<stringPerson> dictionary = new Dictionary<stringPerson>()
{
    {"Person1", person1},
    {"Person2", person2}
};

Oraz wynikowi serializacji tego obiektu:

<ArrayOfKeyValueOfstringPRSLjh4bohd
xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <KeyValueOfstringPRSLjh4bohd>
    <Key>Person1</Key>
    <Value>
      <Age xmlns="">30</Age>
      <FN xmlns="">Gordon</FN>
      <LN xmlns="">Hottentot</LN>
    </Value>
  </KeyValueOfstringPRSLjh4bohd>
  <KeyValueOfstringPRSLjh4bohd>
    <Key>Person2</Key>
    <Value><Age xmlns="">28</Age>
    <FN xmlns="">Paul</FN>
    <LN xmlns="">Stealhanded</LN></Value>
  </KeyValueOfstringPRSLjh4bohd>
</ArrayOfKeyValueOfstringPRSLjh4bohd>

Trzeba przyznać, że sposób serializacji tego obiektu jest dość złożony.

Osoby tworzące biblioteki łaczone dynamicznie (DLL) w C++ zapewne pamiętają problemy związane z dodawaniem przez kompilator różnych przyrostków. Zwało się to międleniem nazw i przysparzało mnóstwa problemów. Czy podczas serializacji słowników jesteśmy skazani na takie cuda i dziwy? Puryści mają tu pole do popisu. Przyjrzyjmy się poniższym wycinkom z kodu:

//Deklaracja typu
[CollectionDataContract
(Name = "PersonDictionary",
ItemName = "Person",
KeyName = "Key",
ValueName = "Value",
Namespace="")]
public class MyDictionaryDictionary<stringPerson>
{
}

//Tworzenie obiektu
MyDictionary dictionary = new MyDictionary
{
    {"Person1", person1},
    {"Person2", person2}
};

Sposób serializacji nie zmienił się, więc nie powielałem niepotrzebnie kodu. Odpowiedni atrybut danych sprawia, że mamy kontrolę prawie nad wszystkim i możemy osiągnąć taki oto rezultat serializacji:

<PersonDictionary
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Person>
    <Key>Person1</Key>
    <Value><Age>30</Age><FN>Gordon</FN><LN>Hottentot</LN></Value>
  </Person>
  <Person>
    <Key>Person2</Key>
    <Value><Age>28</Age><FN>Paul</FN><LN>Stealhanded</LN></Value>
  </Person>
</PersonDictionary>

Tym razem osiągnęliśmy coś znacznie więcej niż tylko ograniczenie rozmiaru komunikatu. Moim zdaniem, co jeszcze cenniejsze, komunikat jest teraz znacznie czytelniejszy.

Tak jak wcześniej, w przypadku list, gdzie wszystkie obiekty implementujące IList serializowane były identycznie, tak teraz, w przypadku słowników, wszystkie klasy implementujące IDictionary będą również serializowane jednakowo (o ile nie zastosujemy dodatkowych atrybutów).

Pomijanie wartości domyślnych

Słyszałem już o różnych optymalizacjach WCF i jak zwykle - są lepsze, są gorsze. Są nawet takie, które działają dokładnie odwrotnie - wpływają wręcz na spadek wydajności.

Gdy druga strona odbiera zserializowany komunikat, musi sobie przesyłany obiekt odtworzyć. Tutaj pojawia się kolejny pomysł optymalizacji - od razu powiem, dobry pomysł. Po co serializować obiekty, które nie mają wartości (null) lub mają wartość domyślną? Jeżeli ktoś wpadł na ten sam pomysł to gratuluję. Wpadli też na niego twórcy WCF. Jak to działa? Wszystko zaczyna się od atrybutu DataMember i jego parametru EmitDefaultValue. Popatrzmy na poniższy przykład deklaracji klasy:

[DataContract(Namespace="", Name="PRS")]
public class Person
{
    [DataMember(Name="FN", EmitDefaultValue=false)]
    public string FirstName { getset; }

    [DataMember(Name = "LN", EmitDefaultValue = false)]
    public string LastName { getset; }

    [DataMember]
    public int Age { getset; }
}

Aby zobaczyć efekty działania należy jeszcze zmienić dane wejściowe:

Person person1 = new Person()
{
    FirstName = "Gordon",
    LastName = "Hottentot",
    Age = 30
};
Person person2 = new Person()
{
    FirstName = null,
    LastName = null,
    Age = 28
};
MyDictionary dictionary = new MyDictionary
{
    {"Person1", person1},
    {"Person2", person2}
};

Warto zwrócić uwagę, że pola FirstName i LastName mają null. Jest to wartość domyślna dla typu, dlatego może zostać spokojnie pominięta. Wywołując konstruktor domyślny obiektu również otrzymalibyśmy null w tych właściwościach. Popatrzmy jeszcze na rezultat serializacji:

<PersonDictionary
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Person>
    <Key>Person1</Key>
    <Value><Age>30</Age><FN>Gordon</FN><LN>Hottentot</LN></Value>
  </Person>
  <Person>
    <Key>Person2</Key>
    <Value><Age>28</Age></Value>
  </Person>
</PersonDictionary>

Pokazany przykład optymalizacji jest chyba najmniej kontrowersyjny z wszystkich - ograniczamy transfer, a nie zmniejszamy czytelności.

Zrozumieć serializację WCF

Zacząłęm od tego, że serializacja jest podstawą komunikacji. Tak też zakończę. Powiem nawet więcej - jest też tym elementem, który warto dobrze zrozumieć. To tutaj, moim zdaniem, jest źródło wielu problemów. Tutaj rodzi się problem limitów transmisji, limitów obiektów w drzewie serializacji, referencji cyklicznych. To od serializacji zależy rozmiar pakietów danych i w konsekwencji wydajność aplikacji końcowych. Często słyszy się o ograniczaniu rozmiaru stron internetowych i wpływu szybkości ładowania się stron na liczbę użytkowników. W WCF jest podobnie, ale o tym nie słychać. Żądanie GET strony WWW wcale tak bardzo się nie różni od żądania WCF.

Zrozumienie podstaw serializacji doprowadza nas do jeszcze ciekawszych obszarów transmisji WCF - rozdziału danych komunikatu na nagłówek i ciało, filtrowanie żądań, niskopoziomową obsługę danych i własne protokoły. O tym jednak innym razem. Temat z pewnością zostanie rozwinięty.

Kategoria:C#Windows Communication FoundationSerializacja

, 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?