Obsah
1. Komunikace realizovaná úsporným binárním formátem MessagePack
3. Některá omezení formátu MessagePack
4. Alternativní binární formáty
5. Praktická část – uložení hodnot různých typů do formátu MessagePack
8. Serializace hodnot true a false
9. Serializace celočíselných hodnot
10. Hodnoty s plovoucí řádovou čárkou
16. Celočíselné hodnoty se znaménkem vs. hodnoty bez znaménka
19. Repositář s demonstračními příklady
1. Komunikace realizovaná úsporným binárním formátem MessagePack
V dnešním článku se zaměříme na popis serializačního formátu MessagePack. Jedná se o jeden z formátů určených pro serializaci a deserializaci dat různých typů s jejich případným přenosem do jiné aplikace či služby. Přenosem se přitom v tomto kontextu myslí jak lokální komunikace, tak i přenos do služby běžící na jiném počítači. Již dříve jsme se ve stručnosti seznámili s využitím formátu JSON a nepřímo taktéž s formátem TOML používaným typicky pro konfigurační soubory (a mnohem méně často pro rozsáhlejší data). V případě JSONu se jedná o poměrně důležitý formát, protože JSON (a samozřejmě též XML) se v současnosti používá v mnoha webových službách a i když stále vznikají a jsou postupně adaptovány další formáty, ať již textové (YAML, edn) či binární (BSON, B-JSON, Smile, Protocol-Buffers), CBOR atd., je velmi pravděpodobné, že se JSON bude i nadále poměrně masivně využívat. Nicméně pochopitelně existují situace, v nichž je vhodné textový a relativně neúsporný JSON nahradit právě nějakým binárním formátem.
I přesto, že se s výše uvedenými formáty JSON a XML setkáme prakticky ve všech oblastech moderního IT, nemusí se vždy jednat o to nejlepší možné řešení problému přenosu strukturovaných dat. Tyto formáty totiž data neukládají v kompaktní binární podobě a navíc je parsing numerických hodnot relativně zdlouhavý, což se projevuje zejména tehdy, pokud je nutné zpracovat skutečně obrovské množství dat (buď mnoho malých zpráv či událostí, nebo naopak rozsáhlé datové soubory). A právě v těchto situacích může být výhodnější sáhnout po nějakém vhodně navrženém binárním formátu. Těch již dnes existuje velké množství, od staršího a dosti těžkopádného ASN.1 (Abstract Syntax Notation One) po formáty, které se snaží napodobit některé vlastnosti JSONu. Příkladem může být formát CBOR, jenž je mj. podporován knihovnou https://github.com/fxamacker/cbor, popř. formát BSON. A konečně, ve se především ve světě Go setkáme i s formátem nazvaným gob neboli Go Objects.
Jednou z „binárních alternativ“ k formátu JSON je u formát MessagePack, s jehož základními vlastnostmi se seznámíme v navazujících kapitolách.
2. Formát MessagePack
Formát MessagePack je navržen takovým způsobem, aby byl „binárním protějškem“ známého a velmi často využívaného formátu JSON, ovšem s několika vylepšeními. Binární formát MessagePack umožňuje serializovat (ukládat) následující datové typy a pochopitelně i jejich kombinace (protože mnohé datové typy jsou vlastně kontejnery pro hodnoty dalších typů):
- Hodnotu nil odpovídající v JSONu hodnotě null
- Pravdivostní hodnoty true a false
- Celá čísla (integer) s různou binární délkou (malé hodnoty jsou uloženy v optimalizované podobě)
- Čísla s plovoucí řádovou čárkou v jednoduché i dvojité přesnosti (včetně všech speciálních hodnot)
- Řetězce, přičemž krátké řetězce jsou uloženy optimalizovaně
- Sekvence bajtů
- Pole, jejichž prvky jsou prakticky jakéhokoli typu
- Mapy, jejichž klíče i prvky jsou prakticky jakéhokoli typu (rozšíření JSONu)
- Časová razítka (to je důležité, JSON tuto možnost postrádá)
- Rozšíření (dvojice s typovou informací a hodnotou)
3. Některá omezení formátu MessagePack
Možnosti formátu MessagePack skutečně do značné míry odpovídají možnostem JSONu s několika rozšířeními zmíněnými výše. Ovšem musíme se zmínit i o některých principiálních omezeních, z nichž některé jsou společné i dalším často používaným serializačním formátům (nehledě na to, zda jsou textové či binární):
- celá čísla mohou nabývat hodnoty z rozsahu –263 až 264-1 (to není chyba – pro kladné hodnoty existuje formát bez znaménka)
- maximální délka řetězců je rovna 4GB (což v praxi nebude velké omezení)
- maximální délka binárního bloku je taktéž rovna 4GB (což již může vadit)
- maximální počet prvků v poli je roven 232-1
- maximální počet dvojic klíč-hodnota v mapě je roven 232-1
- nelze ukládat ukazatele a tím pádem ani přímo pracovat se stromy, obecnými grafy atd. Tento nedostatek se částečně dá nahradit mapami.
- co je ze sémantického hlediska poněkud problematické – není podporován typ „množina“
4. Alternativní binární formáty
Jak jsme se již zmínili v úvodní kapitole, existuje ve skutečnosti mnohem větší množství binárních formátů používaných jak pro serializaci dat, tak i pro komunikaci mezi různými službami (resp. přesněji řečeno pro posílání dat/zpráv mezi službami). Alespoň krátce se tedy o některých z těchto formátů zmiňme.
Prvním alternativním binárním formátem, s nímž se setkáme, je formát nazvaný gob neboli Go Objects. Jedná se o formát určený primárně pro použití v programovacím jazyku Go, což znamená, že jeho využití je relativně specifické (ukládání rozsáhlých dat, komunikace mezi dvojicí služeb naprogramovaných v Go atd.). Tento formát umožňuje serializaci prakticky jakékoli datové struktury, ovšem je ho možné použít i pro primitivní datové typy, resp. pro jejich hodnoty.
Dalším binárním formátem určeným pro přenos prakticky libovolně strukturovaných dat je formát nazvaný CBOR neboli plným jménem Concise Binary Object Representation. Tímto formátem, jenž se snaží nabízet podobné vlastnosti jako JSON (až na možnost jeho přímého čtení člověkem), se budeme zabývat v navazujícím textu (interně je nepatrně složitější než MessagePack).
Dalším sice relativně novým, ale postupně se rozšiřujícím binárním formátem je formát nazvaný BSON (zde je odkaz na JSON nesporný). Možnosti tohoto formátu jsou již větší, například je podporován typ decimal128 určený pro použití v bankovnictví. Taktéž podporuje uložení časových razítek nebo i kódu v JavaScriptu.
5. Praktická část – uložení hodnot různých typů do formátu MessagePack
V praktické části dnešního článku si ukážeme, jakým způsobem jsou uloženy hodnoty různých typů do dat (souborů, proudů bajtů…) ve formátu MessagePack. Demonstrační příklady budou naprogramovány v jazyku Go, ovšem zvolit je možné jakýkoli jazyk a knihovnu zmíněnou na stránce https://msgpack.org/#languages.
Nejprve získáme knihovnu, která serializaci a deserializaci do formátu MessagePack realizuje:
$ go get github.com/ugorji/go/codec go: downloading github.com/ugorji/go/codec v1.2.6 go: downloading github.com/ugorji/go v1.2.6
Alternativně můžeme vytvořit nový projekt (tedy soubor go.mod):
module msgpack-test go 1.17 require github.com/ugorji/go/codec v1.2.6 // indirect
Pro prohlížení obsahu vytvořených binárních souborů lze použít například nějakou formu hexadecimálního prohlížeče. Hexadecimálních prohlížečů a editorů existuje (pro Linux) relativně velké množství. První dva nástroje nazvané od a hexdump (zkráceně hd) pracují jako relativně jednoduché jednosměrné filtry (navíc bývají nainstalovány společně se základním sadou nástrojů), ovšem další nástroj pojmenovaný xxd již může být použit pro obousměrný převod (filtraci), tj. jak pro transformaci původního binárního souboru do čitelného tvaru (většinou s využitím šestnáctkové soustavy), tak i pro zpětný převod. Díky tomu je možné xxd použít například ve funkci pluginu do běžných textových editorů. Další nástroj pojmenovaný hexdiff dokáže porovnat obsah dvou binárních souborů a poslední zmíněný nástroj mcview je, na rozdíl od předchozí čtveřice, aplikací s interaktivním ovládáním a plnohodnotným textovým uživatelským prostředím.
6. Jednoduché datové typy
Do binárního formátu MessagePack lze ukládat jak hodnoty jednoduchých datových typů, tak i hodnoty složených datových typů (což jsou různé typy kontejnerů). Začneme jednoduchými datovými typy, protože binární formát MessagePack je navržen takovým způsobem, aby byl způsob jejich uložení v co největší míře efektivní – a to nejenom co se týká celkového objemu dat, ale i jednoduchosti nebo naopak složitosti zakódování a dekódování hodnot. Podporovány jsou tyto jednoduché datové typy:
- Typ none s jedinou hodnotou nil
- Typ boolean s hodnotami true a false
- Typ unsigned integer s plně 64 bitovým rozsahem
- Typ signed integer s plně 64 bitovým rozsahem
- Typ float/single/float32 s plovoucí řádovou čárkou
- Typ double/float64 s plovoucí řádovou čárkou
7. Serializace hodnoty nil
Začneme tím nejjednodušším možným příkladem, a to konkrétně způsobem serializace hodnoty nil. Ta je použita stejným způsobem jako hodnota null v JSONu, tedy pro indikaci chybějících dat. Přitom nil není přiřazeno k datovému typu, na rozdíl od samotného jazyka Go. Celý příklad se skládá z několika operací:
- Vytvoření a otevření nového (binárního) souboru pro zápis s kontrolou, zda byla operace úspěšná
- Konstrukce objektu/struktury použité pro serializaci
- Vlastní serializace dat, opět s kontrolou, zda byla operace úspěšná
Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:
package main import ( "log" "os" "github.com/ugorji/go/codec" ) const filename = "/tmp/nil.bin" func main() { // vytvořit soubor s binárními daty fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { log.Fatal(err) } defer fout.Close() log.Print("Output file created") // handler var handler codec.MsgpackHandle // objekt realizující zakódování dat encoder := codec.NewEncoder(fout, &handler) log.Print("Encoder created") // zakódování dat err = encoder.Encode(nil) if err != nil { log.Fatal(err) } log.Print("Done") }
Výsledkem bude binární soubor obsahující jediný bajt:
$ od -A x -t x1 -v nil.bin 000000 c0 000001
8. Serializace hodnot true a false
Ve formátu MessagePack jsou plně podporovány i hodnoty true a false, což znamená, že není nutné (ani ze sémantického pohledu rozumné) používat pro reprezentaci pravdivostních hodnot například hodnoty 0 a 1 či 0 a –1. Navíc jsou pravdivostní hodnoty uloženy relativně rozumným způsobem – v jediném bajtu. O tom se budeme moci velmi snadno přesvědčit:
package main import ( "log" "os" "github.com/ugorji/go/codec" ) const filename = "/tmp/true.bin" func main() { // vytvořit soubor s binárními daty fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { log.Fatal(err) } defer fout.Close() log.Print("Output file created") // handler var handler codec.MsgpackHandle // objekt realizující zakódování dat encoder := codec.NewEncoder(fout, &handler) log.Print("Encoder created") // zakódování dat err = encoder.Encode(true) if err != nil { log.Fatal(err) } log.Print("Done") }
Nyní se podívejme na to, jak jsou tyto dvě hodnoty uloženy do výsledného souboru:
$ od -A x -t x1 -v true.bin 000000 c3 000001 $ od -A x -t x1 -v false.bin 000000 c2 000001
Což opět plně odpovídá specifikaci.
// zakódování dat err = encoder.Encode(false) if err != nil { log.Fatal(err) }
a:
// zakódování dat err = encoder.Encode(true) if err != nil { log.Fatal(err) }
9. Serializace celočíselných hodnot
Prozatím dosti nudné téma poněkud zpestříme, protože si ukážeme způsob serializace celočíselných hodnot. V tomto ohledu museli tvůrci formátu MessagePack splnit dva protichůdné požadavky:
- reprezentovat co největší rozsah hodnot, ideálně 64bitové hodnoty
- na druhou stranu je použití 64bitů (8 bajtů) ve všech případech až trestuhodné plýtvání místem (a to i oproti textovému JSONu)
Výsledkem snahy o splnění obou požadavků je flexibilní způsob uložení celých čísel v jednom, dvou, třech, pěti či devíti bajtech – vždy v závislosti na konkrétní hodnotě a taktéž na tom, zda se jedná o hodnotu kladnou či zápornou. Specifikace uložení celých čísel ve skutečnosti není příliš složitá a můžeme si ji snadno otestovat.
Uložení malého celého čísla:
// zakódování dat err = encoder.Encode(42) if err != nil { log.Fatal(err) }
Výsledkem je v tomto případě pouhý jeden bajt, který obsahuje jak informace o datovém typu, tak i vlastní hodnotu:
$ od -A x -t x1 -v small_int.bin 000000 2a 000001
Uložení čísla většího než 127, ale menšího než 216:
// zakódování dat err = encoder.Encode(1000) if err != nil { log.Fatal(err) }
Nyní je hodnota uložena do třech bajtů. V prvním bajtu je deklarace typu, druhé dva bajty reprezentují hodnotu 0×3e8 = 1000:
$ od -A x -t x1 -v longer_int.bin 000000 d1 03 e8 000003
Číslo větší než 216:
// zakódování dat err = encoder.Encode(100000) if err != nil { log.Fatal(err) }
Opět je použit jeden bajt se specifikací typu, za kterým následuje čtveřice bajtů 0×0186a0 = 100000:
$ od -A x -t x1 -v even_longer_int.bin 000000 d2 00 01 86 a0 000005
A konečně hodnota 260:
// zakódování dat err = encoder.Encode(2 << 60) if err != nil { log.Fatal(err) }
Jedná se o 64bitovou hodnotu uloženou v devíti bajtech:
$ od -A x -t x1 -v long_int.bin 000000 d3 20 00 00 00 00 00 00 00 000009
10. Hodnoty s plovoucí řádovou čárkou
Ve formátu MessagePack jsou podle očekávání podporovány i hodnoty s plovoucí řádovou čárkou. Jedná se jak o hodnoty s jednoduchou přesností (single, float, float32), tak i o hodnoty s dvojitou přesností (double, float64). Nejprve si ukažme způsob uložení hodnot s jednoduchou přesností, což v jazyce Go odpovídá datovému typu float32:
// zakódování dat err = encoder.Encode(float32(3.14)) if err != nil { log.Fatal(err) }
Výsledkem je soubor s pěti bajty. První bajt opět obsahuje typ dat, další čtyři bajty pak vlastní hodnotu:
$ od -A x -t x1 -v single.bin 000000 ca 40 48 f5 c3 000005
Uložení hodnoty s dvojitou přesností:
// zakódování dat err = encoder.Encode(3.14) if err != nil { log.Fatal(err) }
Výsledkem je soubor s devíti bajty, jehož struktura je (až na odlišný typ) totožná s předchozím souborem:
$ od -A x -t x1 -v double.bin 000000 cb 40 09 1e b8 51 eb 85 1f 000009
11. Složené datové typy
Po popisu způsobu uložení jednoduchých datových typů (což nebylo nic složitého) si ukážeme, jakým způsobem je v MessagePacku realizováno uložení složených datových typů. Do této kategorie se řadí především řetězce, sekvence bajtů, pole, ale v neposlední řadě i velmi důležité mapy, které lze použít například pro uložení atributů objektů. Opět uvidíme, že u některých výše zmíněných datových typů je dbáno na efektivitu výsledného binárního souboru, a to jak z hlediska celkového objemu dat, tak i složitosti kódování a dekódování těchto dat.
12. Krátké řetězce
Takřka nepostradatelným složeným datovým typem jsou řetězce. Interně se pro jejich uložení používá UTF-8. Neméně důležitá je však informace o tom, jak dlouhý řetězec je. Délka řetězce je uložena před vlastní znaky a to konkrétně tak, že pro krátké řetězce je délka uložena přímo v bajtu se specifikací typu (tedy neztratíme ani jediný bajt!) a pro delší řetězce je délka uložena v jednom, dvou či čtyřech bajtech.
Velmi krátký řetězec, menší než 31 bajtů (nikoli znaků!):
const message = "Hello" // zakódování dat err = encoder.Encode(message) if err != nil { log.Fatal(err) }
V tomto případě je délka řetězce uložena v prvním bajtu, přičemž první tři bity tohoto bajtu určují datový typ:
$ od -A x -t x1z -v short_string.bin 000000 a5 48 65 6c 6c 6f <.Hello> 000006
13. Dlouhé řetězce
Vyzkoušejme si nyní poněkud delší řetězec:
const message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." // zakódování dat err = encoder.Encode(message) if err != nil { log.Fatal(err) }
V tomto případě je první bajt roven konstantě 0×da. Za ní následují dva bajty s délkou řetězce v bajtech, konkrétně celkem 0×e7=231 bajtů. A poté již vlastní znaky tvořící řetězec:
$ od -A x -t x1z -v longer_string.bin 000000 da 00 e7 4c 6f 72 65 6d 20 69 70 73 75 6d 20 64 >...Lorem ipsum d< 000010 6f 6c 6f 72 20 73 69 74 20 61 6d 65 74 2c 20 63 >olor sit amet, c< 000020 6f 6e 73 65 63 74 65 74 75 72 20 61 64 69 70 69 >onsectetur adipi< 000030 73 63 69 6e 67 20 65 6c 69 74 2c 20 73 65 64 20 >scing elit, sed < 000040 64 6f 20 65 69 75 73 6d 6f 64 20 74 65 6d 70 6f >do eiusmod tempo< 000050 72 20 69 6e 63 69 64 69 64 75 6e 74 20 75 74 20 >r incididunt ut < 000060 6c 61 62 6f 72 65 20 65 74 20 64 6f 6c 6f 72 65 >labore et dolore< 000070 20 6d 61 67 6e 61 20 61 6c 69 71 75 61 2e 20 55 > magna aliqua. U< 000080 74 20 65 6e 69 6d 20 61 64 20 6d 69 6e 69 6d 20 >t enim ad minim < 000090 76 65 6e 69 61 6d 2c 20 71 75 69 73 20 6e 6f 73 >veniam, quis nos< 0000a0 74 72 75 64 20 65 78 65 72 63 69 74 61 74 69 6f >trud exercitatio< 0000b0 6e 20 75 6c 6c 61 6d 63 6f 20 6c 61 62 6f 72 69 >n ullamco labori< 0000c0 73 20 6e 69 73 69 20 75 74 20 61 6c 69 71 75 69 >s nisi ut aliqui< 0000d0 70 20 65 78 20 65 61 20 63 6f 6d 6d 6f 64 6f 20 >p ex ea commodo < 0000e0 63 6f 6e 73 65 71 75 61 74 2e >consequat.< 0000ea
Zakódování řetězce, který je skutečně delší než 256 bajtů, ale kratší než 216 bajtů:
const message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." // zakódování dat err = encoder.Encode(message) if err != nil { log.Fatal(err) }
S výsledkem:
$ od -A x -t x1z -v even_longer_string.bin 000000 da 01 bd 4c 6f 72 65 6d 20 69 70 73 75 6d 20 64 >...Lorem ipsum d< 000010 6f 6c 6f 72 20 73 69 74 20 61 6d 65 74 2c 20 63 >olor sit amet, c< 000020 6f 6e 73 65 63 74 65 74 75 72 20 61 64 69 70 69 >onsectetur adipi< 000030 73 63 69 6e 67 20 65 6c 69 74 2c 20 73 65 64 20 >scing elit, sed < 000040 64 6f 20 65 69 75 73 6d 6f 64 20 74 65 6d 70 6f >do eiusmod tempo< 000050 72 20 69 6e 63 69 64 69 64 75 6e 74 20 75 74 20 >r incididunt ut < 000060 6c 61 62 6f 72 65 20 65 74 20 64 6f 6c 6f 72 65 >labore et dolore< 000070 20 6d 61 67 6e 61 20 61 6c 69 71 75 61 2e 20 55 > magna aliqua. U< 000080 74 20 65 6e 69 6d 20 61 64 20 6d 69 6e 69 6d 20 >t enim ad minim < 000090 76 65 6e 69 61 6d 2c 20 71 75 69 73 20 6e 6f 73 >veniam, quis nos< 0000a0 74 72 75 64 20 65 78 65 72 63 69 74 61 74 69 6f >trud exercitatio< 0000b0 6e 20 75 6c 6c 61 6d 63 6f 20 6c 61 62 6f 72 69 >n ullamco labori< 0000c0 73 20 6e 69 73 69 20 75 74 20 61 6c 69 71 75 69 >s nisi ut aliqui< 0000d0 70 20 65 78 20 65 61 20 63 6f 6d 6d 6f 64 6f 20 >p ex ea commodo < 0000e0 63 6f 6e 73 65 71 75 61 74 2e 20 44 75 69 73 20 >consequat. Duis < 0000f0 61 75 74 65 20 69 72 75 72 65 20 64 6f 6c 6f 72 >aute irure dolor< 000100 20 69 6e 20 72 65 70 72 65 68 65 6e 64 65 72 69 > in reprehenderi< 000110 74 20 69 6e 20 76 6f 6c 75 70 74 61 74 65 20 76 >t in voluptate v< 000120 65 6c 69 74 20 65 73 73 65 20 63 69 6c 6c 75 6d >elit esse cillum< 000130 20 64 6f 6c 6f 72 65 20 65 75 20 66 75 67 69 61 > dolore eu fugia< 000140 74 20 6e 75 6c 6c 61 20 70 61 72 69 61 74 75 72 >t nulla pariatur< 000150 2e 20 45 78 63 65 70 74 65 75 72 20 73 69 6e 74 >. Excepteur sint< 000160 20 6f 63 63 61 65 63 61 74 20 63 75 70 69 64 61 > occaecat cupida< 000170 74 61 74 20 6e 6f 6e 20 70 72 6f 69 64 65 6e 74 >tat non proident< 000180 2c 20 73 75 6e 74 20 69 6e 20 63 75 6c 70 61 20 >, sunt in culpa < 000190 71 75 69 20 6f 66 66 69 63 69 61 20 64 65 73 65 >qui officia dese< 0001a0 72 75 6e 74 20 6d 6f 6c 6c 69 74 20 61 6e 69 6d >runt mollit anim< 0001b0 20 69 64 20 65 73 74 20 6c 61 62 6f 72 75 6d 2e > id est laborum.< 0001c0
14. Krátká pole
Pole, a to pole prvků libovolných typů, se do formátu MessagePack opět ukládá podle toho, kolik prvků takové pole obsahuje. Pole s prvky, jejichž počet nepřesáhne patnáct, obsahuje pouze jediný bajt navíc. Obsah tohoto bajtu určuje, že se jedná o pole a současně i ve spodních čtyřech bitech obsahuje počet prvků pole.
var values []int = []int{1, 2, 3, 4} // zakódování dat err = encoder.Encode(values) if err != nil { log.Fatal(err) }
Výše uvedené pole se čtyřmi prvky je uloženo v pouhých pěti bajtech a to z toho důvodu, že hodnoty prvků samy o sobě mají tak malou hodnotu, že každý z nich může být uložen v jediném bajtu:
$ od -A x -t x1 -v short_array.bin 000000 94 01 02 03 04 000005
Naproti tomu druhé serializované pole již obsahuje prvky s relativně vyššími hodnotami:
var values []int = []int{100, 200, 300, 400} // zakódování dat err = encoder.Encode(values) if err != nil { log.Fatal(err) }
Nyní bude soubor delší, protože již některé prvky nelze uložit do jediného bajtu:
$ od -A x -t x1 -v short_array2.bin 000000 94 64 d1 00 c8 d1 01 2c d1 01 90 00000b
V tomto případě první bajt obsahuje typ (pole) s jeho délkou. Následuje bajt s hodnotou 0×64=100, tedy první prvek (jediný bajt), další prvek je uložen ve třech bajtech (0×d1 = typ, 0×00c8=200 je hodnota) atd.
15. Delší pole
Pole delší než patnáct prvků se dále rozlišují podle toho, zda je celkový počet prvků menší než 216-1 nebo větší než tato hodnota. Podle počtu prvků se volí počet bajtů pro uložení délky pole – dva či čtyři bajty. My si dnes ukážeme pouze první typ, tj. pole menší než 216-1 prvků:
const N = 1000 var values [N]int for i := 0; i < N; i++ { values[i] = i } // zakódování dat err = encoder.Encode(values) if err != nil { log.Fatal(err) }
Obsah výsledného binárního souboru si zobrazíme:
$ od -A x -t x1 -v array16.bin
Nejprve je uveden typ (0×dc) a počet prvků 0×03e8=1000. Dále již následují hodnoty jednotlivých prvků. Pro prvních 128 prvků postačuje pro uložení použít jediný bajt:
000000 dc 03 e8 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 000010 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 000020 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 000030 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 000040 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 000050 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 000060 5d 5e 5f 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 000070 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 000080 7d 7e 7f
Větší hodnoty jsou již uloženy složitějším způsobem, protože první bajt každé trojice obsahuje typ (0×d1):
000080 d1 00 80 d1 00 81 d1 00 82 d1 00 83 d1 000090 00 84 d1 00 85 d1 00 86 d1 00 87 d1 00 88 d1 00 0000a0 89 d1 00 8a d1 00 8b d1 00 8c d1 00 8d d1 00 8e 0000b0 d1 00 8f d1 00 90 d1 00 91 d1 00 92 d1 00 93 d1
Často se setkáme s polem bajtů, a to mj. i proto, že takové pole může vzniknout například jako výsledek zašifrování dat:
const N = 1000 var values [N]byte for i := 0; i < N; i++ { values[i] = byte(i) }
Takové pole bude zapsáno ve formátu:
$ od -A x -t x1 -v bytes.bin 000000 da 03 e8 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 000010 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 000020 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c
Celý soubor bude mít délku 1003 bajtů, tedy tři bajty pro určení typu (pole) a jeho délky následovanou 1000 bajty představujícími data.
16. Celočíselné hodnoty se znaménkem vs. hodnoty bez znaménka
Vraťme se ještě k poli 1000 prvků typu int, které jsme vytvořili a uložili tímto způsobem:
const N = 1000 var values [N]int for i := 0; i < N; i++ { values[i] = i }
Výsledkem byl soubor o délce 2747 bajtů, přičemž všechny hodnoty větší než 127 byly uloženy ve třech bajtech.
Pokud ovšem namísto typu int použijeme typ uint:
const N = 1000 var values [N]uint for i := 0; i < N; i++ { values[i] = uint(i) }
Budou ty samé hodnoty 0 až 999 uloženy v souboru o délce 2619 bajtů a způsob uložení se bude lišit:
- Hodnoty menší než 128 budou uloženy v jediné bajtu
- Hodnoty mezi 128 až 255 budou uloženy jako dvojice 0×cc + hodnota
- Hodnoty větší než 255 budou uloženy jako trojice 0×cd + hodnota
Můžeme se o tom snadno přesvědčit:
$ od -A x -t x1 -v array16B.bin Typ + délka pole + prvních 128 prvků:000000 dc 03 e8 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 000010 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 000020 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 000030 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 000040 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 000050 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 000060 5d 5e 5f 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 000070 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 000080 7d 7e 7f
Následují prvky s hodnotou 128 až 255:
000080 cc 80 cc 81 cc 82 cc 83 cc 84 cc 85 cc 000090 86 cc 87 cc 88 cc 89 cc 8a cc 8b cc 8c cc 8d cc 0000a0 8e cc 8f cc 90 cc 91 cc 92 cc 93 cc 94 cc 95 cc
A poté zbylé prvky:
000180 fe cc ff cd 01 00 cd 01 01 cd 01 02 cd 01 03 cd 000190 01 04 cd 01 05 cd 01 06 cd 01 07 cd 01 08 cd 01 0001a0 09 cd 01 0a cd 01 0b cd 01 0c cd 01 0d cd 01 0e 0001b0 cd 01 0f cd 01 10 cd 01 11 cd 01 12 cd 01 13 cd
17. Serializace map
Ve formátu JSON se prakticky vždy setkáme s mapami resp. s asociativními poli. Tuto datovou strukturu lze použít i v MessagePacku a to dokonce ještě ve vylepšené variantě, protože klíči mohou být hodnoty jakéhokoli typu, nejenom řetězce. Ukažme si ovšem základní použití s řetězci jako klíči:
var m map[string]int = make(map[string]int) m["foo"] = 1 m["bar"] = 2 // zakódování dat err = encoder.Encode(m) if err != nil { log.Fatal(err) }
Tato mapa se dvěma dvojicemi klíč+hodnota bude uložena v pouhých jedenácti bajtech (v JSONu se nedostaneme pod 17 bajtů – ostatně sami si to vyzkoušejte na https://msgpack.org/ po výběru „Try“):
$ od -A x -t x1 -v map.bin 000000 82 a3 66 6f 6f 01 a3 62 61 72 02 00000b
Mapa obsahuje dvě dvojice, což je malý počet. Z tohoto důvodu je typ (mapa) i počet dvojic klíč-hodnota uložena v jediném bajtu 0×80+0×02=0×82. Následuje stejný obsah, jako v případě polí:
Bajty | Význam |
---|---|
a3 66 6f 6f | řetězec „foo“ o délce tří bajtů |
01 | malé celé číslo 1 |
a3 62 61 72 | řetězec „bar“ o délce tří bajtů |
02 | malé celé číslo 2 |
18. Co dále?
Pro praktické použití formátu MessagePack musíme vyřešit ještě další problémy, s nimiž se dennodenně můžeme setkat v praxi. Týká se to zejména serializace struktur a v neposlední řadě taktéž rekurzivních struktur typu binární strom. S touto problematikou se blíže seznámíme příště, kde MessagePack kromě jiného využijeme i z dalších programovacích jazyků. Taktéž si vysvětlíme způsob uložení časových značek s různou přesností.
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Základní informace o MessagePacku
https://msgpack.org/ - MessagePack na Wikipedii
https://en.wikipedia.org/wiki/MessagePack - Comparison of data-serialization formats (Wikipedia)
https://en.wikipedia.org/wiki/Comparison_of_data-serialization_formats - Repositáře msgpacku
https://github.com/msgpack - Specifikace ukládání různých typů dat
https://github.com/msgpack/msgpack/blob/master/spec.md - Podpora MessagePacku v různých jazycích
https://msgpack.org/#languages - Základní implementace formátu msgpack pro Go
https://github.com/msgpack/msgpack-go - go-codec
https://github.com/ugorji/go - Gobs of data
https://blog.golang.org/gobs-of-data - Formát BSON
http://bsonspec.org/ - Problematika nulových hodnot v Go, aneb proč nil != nil
https://www.root.cz/clanky/problematika-nulovych-hodnot-v-go-aneb-proc-nil-nil/ - IEEE-754 Floating Point Converter
https://www.h-schmidt.net/FloatConverter/IEEE754.html - Base Convert: IEEE 754 Floating Point
https://baseconvert.com/ieee-754-floating-point - Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/