Hlavní navigace

Komunikace realizovaná úsporným binárním formátem MessagePack

18. 1. 2022
Doba čtení: 24 minut

Sdílet

 Autor: Depositphotos
Pro komunikaci mezi službami se v současnosti využívá relativně velké množství (serializačních) formátů. Ty můžeme rozdělit na formáty textové (JSON, XML) a binární. Mezi binární formáty patří i MessagePack.

Obsah

1. Komunikace realizovaná úsporným binárním formátem MessagePack

2. Formát 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

6. Jednoduché datové typy

7. Serializace hodnoty nil

8. Serializace hodnot true a false

9. Serializace celočíselných hodnot

10. Hodnoty s plovoucí řádovou čárkou

11. Složené datové typy

12. Krátké řetězce

13. Dlouhé řetězce

14. Krátká pole

15. Delší pole

16. Celočíselné hodnoty se znaménkem vs. hodnoty bez znaménka

17. Serializace map

18. Co dále?

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

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ů):

  1. Hodnotu nil odpovídající v JSONu hodnotě null
  2. Pravdivostní hodnoty true a false
  3. Celá čísla (integer) s různou binární délkou (malé hodnoty jsou uloženy v optimalizované podobě)
  4. Čísla s plovoucí řádovou čárkou v jednoduché i dvojité přesnosti (včetně všech speciálních hodnot)
  5. Řetězce, přičemž krátké řetězce jsou uloženy optimalizovaně
  6. Sekvence bajtů
  7. Pole, jejichž prvky jsou prakticky jakéhokoli typu
  8. Mapy, jejichž klíče i prvky jsou prakticky jakéhokoli typu (rozšíření JSONu)
  9. Časová razítka (to je důležité, JSON tuto možnost postrádá)
  10. 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í):

  1. celá čísla mohou nabývat hodnoty z rozsahu –263 až 264-1 (to není chyba – pro kladné hodnoty existuje formát bez znaménka)
  2. maximální délka řetězců je rovna 4GB (což v praxi nebude velké omezení)
  3. maximální délka binárního bloku je taktéž rovna 4GB (což již může vadit)
  4. maximální počet prvků v poli je roven 232-1
  5. maximální počet dvojic klíč-hodnota v mapě je roven 232-1
  6. 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.
  7. co je ze sémantického hlediska poněkud problematické – není podporován typ „množina“
Poznámka: podrobnosti o tom, jak jsou ukládány jednotlivé typy hodnot, si ukážeme ve druhé (prakticky zaměřené) části článku.

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.

Poznámka: zajímavé je, že ani jeden z uvedených binárních formátů nepodporuje typ bfloat16, i když zrovna v této oblasti se jeho použití přímo nabízí.

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
Poznámka: lze použít i další knihovny, například tuto. Výše uvedenou knihovnu jsem vybral mj. i proto, že podporuje i další formáty a navíc je možné provádět serializaci a deserializaci dat do/z kanálu a tedy přímo v Go velmi snadno realizovat proudové zpracování dat (streaming.

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.

Poznámka: dnes si vystačíme s možnostmi nabízenými nástrojem od neboli octal dump. Jméno tohoto nástroje je ve skutečnosti zavádějící, protože dokáže zobrazit obsah binárního soubory mnoha různými způsoby. Již fakt, že jméno této utility má pouhá dvě písmena, napovídá, že se jedná o nástroj pocházející již z prvních verzí Unixu. Původní varianty utility od vypisovaly obsah zvoleného souboru (alternativně standardního vstupu či zvoleného zařízení) s využitím osmičkové soustavy, ovšem GNU verze od nabízí uživatelům mnohem víc možností, a to včetně včetně použití hexadecimální soustavy (ostatně i proto o této utilitě dnes píšeme), zformátování sousedních čtyř bajtů do čísla typu single/float, dtto pro osm bajtů a čísla typu double apod.

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:

  1. Typ none s jedinou hodnotou nil
  2. Typ boolean s hodnotami truefalse
  3. Typ unsigned integer s plně 64 bitovým rozsahem
  4. Typ signed integer s plně 64 bitovým rozsahem
  5. Typ float/single/float32 s plovoucí řádovou čárkou
  6. 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í:

  1. Vytvoření a otevření nového (binárního) souboru pro zápis s kontrolou, zda byla operace úspěšná
  2. Konstrukce objektu/struktury použité pro serializaci
  3. 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")
}
Poznámka: povšimněte si, že metodě encoder.Encoder je možné předat hodnotu libovolného typu.

Výsledkem bude binární soubor obsahující jediný bajt:

$ od -A x -t x1 -v nil.bin
 
000000 c0
000001

To plně odpovídá specifikaci.

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.

Poznámka: v dalších kapitolách již většinou nebudeme uvádět úplný zdrojový kód demonstračních příkladů. Namísto toho se spokojíme pouze s uvedením té nejdůležitější části – vlastní serializace dat. To znamená, že předchozí dva příklady bychom zkrátili na pouhých několik řádků:
// 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:

  1. reprezentovat co největší rozsah hodnot, ideálně 64bitové hodnoty
  2. 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.

Poznámka: povšimněte si striktního použití pořadí bajtů big endian!

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
Poznámka: podobné příklady lze vytvořit i pro záporná čísla.

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
Poznámka: čtveřici bajtů se zakódovanou hodnotou si můžete ověřit například na této stránce nebo ještě lépe zde po zadání vstupu 3.14.

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
Poznámka: výsledek si můžete ověřit na této stránce.

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
Poznámka: zde by ve skutečnosti knihovna měla resp. mohla zvolit řetězec s délkou menší než 256 bajtů a tedy typ 0×d9 namísto typu 0×9a – viz též https://github.com/msgpac­k/msgpack/blob/master/spec­.md#str-format-family.

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.

Poznámka: můžeme vidět, že každý prvek může být uložen různým způsobem a pole tedy nejsou heterogenní ani s ohledem na datový typ z pohledu programátora (ve skutečnosti se tedy spíše jedná o seznamy) ani z pohledu binárního formátu.

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:

  1. Hodnoty menší než 128 budou uloženy v jediné bajtu
  2. Hodnoty mezi 128 až 255 budou uloženy jako dvojice 0×cc + hodnota
  3. 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í:

Linux tip

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:

# Příklad/soubor Stručný popis Cesta
1 msgpack_nil.go serializace hodnoty nil https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_nil.go
2 msgpack_true.go serializace hodnot true https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_true.go
3 msgpack_false.go serializace hodnot false https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_false.go
4 msgpack_small_int.go serializace celočíselné hodnoty menší než 127 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_small_int.go
5 msgpack_longer_int.go serializace celočíselné hodnoty menší než 216 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_longer_int.go
6 msgpack_even_longer_int.go serializace celočíselné hodnoty větší než 216 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_even_longer_int.go
7 msgpack_long_int.go serializace celočíselné hodnoty větší než 232 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_long_int.go
8 msgpack_single.go serializace hodnoty s plovoucí řádovou čárkou (single) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_single.go
9 msgpack_double.go serializace hodnoty s plovoucí řádovou čárkou (double) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_double.go
       
10 msgpack_short_string.go serializace krátkého řetězce (méně než 31 znaků) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_short_string.go
11 msgpack_longer_string.go serializace delšího řetězce (méně než 256 znaků) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_longer_string.go
12 msgpack_even_longer_string.go serializace dlouhého řetězce https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_even_longer_string.go
       
13 msgpack_short_array1.go krátké pole čtyř hodnot 1–4 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_short_array1.go
14 msgpack_short_array2.go krátké pole čtyř hodnot 100, 200, 300 a 400 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_short_array2.go
15 msgpack_bytes.go pole 1000 bajtů https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_bytes.go
16 msgpack_array_16A.go pole 1000 prvků typu int https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_array_16A.go
17 msgpack_array_16B.go pole 1000 prvků typu uint https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_array_16B.go
       
18 msgpack_map.go serializace mapy https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_map.go

20. Odkazy na Internetu

  1. Základní informace o MessagePacku
    https://msgpack.org/
  2. MessagePack na Wikipedii
    https://en.wikipedia.org/wi­ki/MessagePack
  3. Comparison of data-serialization formats (Wikipedia)
    https://en.wikipedia.org/wi­ki/Comparison_of_data-serialization_formats
  4. Repositáře msgpacku
    https://github.com/msgpack
  5. Specifikace ukládání různých typů dat
    https://github.com/msgpac­k/msgpack/blob/master/spec­.md
  6. Podpora MessagePacku v různých jazycích
    https://msgpack.org/#languages
  7. Základní implementace formátu msgpack pro Go
    https://github.com/msgpack/msgpack-go
  8. go-codec
    https://github.com/ugorji/go
  9. Gobs of data
    https://blog.golang.org/gobs-of-data
  10. Formát BSON
    http://bsonspec.org/
  11. Problematika nulových hodnot v Go, aneb proč nil != nil
    https://www.root.cz/clanky/pro­blematika-nulovych-hodnot-v-go-aneb-proc-nil-nil/
  12. IEEE-754 Floating Point Converter
    https://www.h-schmidt.net/FloatConverter/I­EEE754.html
  13. Base Convert: IEEE 754 Floating Point
    https://baseconvert.com/ieee-754-floating-point
  14. 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/

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.