Nedávno jsem potřeboval využít rychlou a kompaktní serializaci pro přenos dat. Po krátkém průzkumu jsem se rozhodnul pro knihovnu protobuf-net, což je .NET implementace formátu binární serializace, kterou používá Google. Nebylo to zrovna složité hledání vhodné knihovny, o “protocol buffers” už jsem několikrát slyšel a tak jsem spíš hledal srovnání s jinými formáty. Tento článek popisuje problémy, se kterými jsem se potkal při použití této knihovny a co bylo příčinou. Celkově jsem narazil na dva týkající se, jak už nadpis napovídá, třídní hierarchie a jeden týkající se generických typů.
Představení
Pokud jste o tomto formátu serializace ještě neslyšeli, tak tady je krátký úvod. Více informací naleznete různě po internetu, ale začít můžete například v repozitáři oficiálního projektu.
Vlastnosti:
- malý objem dat (řádově menší, než stejná data v XML)
- velmi rychlá serializace i deserializace na klientu i serveru
- nezávislý na platformě
- rozšiřitelný v ohledu přidání nových dat do starých zpráv
Jedná se o binární serializaci, což znamená, že data jsou lidským okem nečitelná (na rozdíl od JSON nebo XML), takže i debuggování je komplikovanější. Na druhou stranu je to ale oproti výše zmíněným dvěma neukecaný, a tudíž velmi kompaktní formát. Řekl bych, že nejvhodnější je pro interní komunikaci, kde je požadován výkon. Nevidím ho vhodný při použití na webu, kde je momentálně velmi populární JSON, se kterým se dobře pracuje jak v JS, tak ho lze přímo ukládat do některých dokumentových databází.
Překryv id
Kompaktnosti dat se v tomto formátu dosahuje tak, že serializovaná data například neobsahují informaci o názvech dat, která jsou přenášena. Aby bylo možné rozeznat data v bufferu, je nutné jednotlivé položky očíslovat unikátními ID a ideálně toto číslování už neměnit. Z toho vyplývá, že u tohoto formátu je nutné předem znát schéma. XML ani JSON toto nevyžadují a lze je parsovat bez znalosti schéma. Uvažujme následující jednoduchý datový model.
[ProtoContract, ProtoInclude(1, typeof(Tiger))]
class Animal
{
[ProtoMember(1)]
public string Name { get; set; }
}
[ProtoContract]
class Tiger : Animal
{
[ProtoMember(2)]
public string Kind { get; set; }
}
Myslím, že není nutné to komentovat, princip je stejný, jako při použití jiných serializátorů v .NET.
var animals = new List<Animal>
{
new Tiger {Name = "Foo", Kind = "Bengal"},
new Tiger {Name = "Bar", Kind = "Malayan"}
};
var buffer = new byte[1024];
using (var stream = new MemoryStream())
{
Serializer.Serialize(stream, animals);
buffer = stream.ToArray();
}
Opět, nic nového zde není. Předpokládal bych, že toto bude normálně fungovat, ale jak jsem se zmýlil. Dal jsem si pozor, aby se ID v rámci třídy nepřekrývala - Name(1), Kind(2), ale přesto dostávám při serializaci následující výjimku.
To bylo moje první “aha”. Skutečnost je taková, že vlastnosti Name i Kind mohou mít obě stejné ID, ale ID v rámci atributů aplikovaných na třídou se nesmí překrývat s ID v atributech aplikovaných na členy třídy.
invalid wire-type
Další, poměrně kryptické chyby jsem se dočkal hned po vyřešení té předchozí.
K odstranění naštěstí pomohla odpověď nacházející se na odkazu uvedeném přímo v chybě. V mém případě se ovšem nejednalo o práci se soubory.
“Since the stack trace references this StackOverflow question, I thought I'd point out that you can also receive this exception if you (accidentally) deserialize a stream into a different type than what was serialized. So it's worth double-checking both sides of the conversation to ensure this is not happening.”
Zkrátka, tuto chybu také můžete potkat při deserializaci neočekávaného typu. Mě to potkalo rovnou ve dvou případech. První situace - přidáme další třídu do hierarchie
[ProtoContract]
class Giraffe : Animal
{
[ProtoMember(1)]
public int NeckLength { get; set; }
}
a taky strčíme nové zvířátko k tygrům do kolekce.
var animals = new List<Animal>
{
new Tiger {Name = "Foo", Kind = "Bengal"},
new Tiger {Name = "Bar", Kind = "Malayan"},
new Giraffe {Name = "Baz", NeckLength = 100}
};
Snadno se stane, že s přidáním nového zděděného typu zapomenete přidat do bázové třídy patřičný atribut ProtoInclude pro nový typ zvířete a ten je pak při deserializaci nerozpoznán.
Druhá situace - já jsem také ze serveru vracel odpověď na požadavek ve formě textu, pokud byl například překročen limit velikosti požadavku. Na to jsem ale zapomněl a půl hodiny jsem hledal, kde může být problém, než jsem si uvědomil, že mi server hází výjimku a odpověď je zakódovaný string.
Generický typ
Poslední problém mě potkal při serializaci generických typů.
[ProtoContract]
class GenericAnimal<T> : Animal
{
[ProtoMember(1)]
public T Property { get; set; }
}
// v tride Animal
ProtoInclude(12, typeof(GenericAnimal<>))
Z téhle chybové hlášky jsem moc moudrý nebyl, naštěstí ji doprovází ještě vnitřní chyba.
{"No serializer defined for type: "}
Když zkusíte vyhledat tuto vnitřní chybu na internetu, první co uvidíte je, že všichni ostatní tam mají uvedený typ, pro který není serializer definovaný. To mě přivedlo k informaci, že všechny definované typy pro serializaci musí být “uzavřené”. Buď tedy předpokládáte použití pouze některých generických parametrů a explicitně je uvedete
ProtoInclude(12, typeof(GenericAnimal<string>)),
ProtoInclude(13, typeof(GenericAnimal<int>)),
...
nebo prostě vytvoříte konkrétní třídy
StringAnimal,
IntAnimal, atd. Pro rozsáhlý doménový model to není zrovna stručné řešení. Víte o lepším? Jak to funguje v jiných serializačních formátech? Podělte se o Vaše zkušenosti se serializací generických typů v komentářích.