Platformok közötti nyelvek közötti bináris adatkódolás, magyarázat

A Protocol Buffers egy Google által kifejlesztett eszközlánc az adatok és objektumok bináris kódolására, amely programozási nyelvek között működik. Ez az alapja a gRPC-nek, egy többnyelvű távoli eljáráshívó rendszernek, de külön is használható.

A szoftverfejlesztés története mindig is az alkalmazások közötti adatmegosztás kihívásával szembesült. Számos technika létezik, például elterjedt a szövegalapú adatformátum, például a JSON, TOML, XML vagy YAML használata. Ezeket a formátumokat a megfelelő könyvtáraknak köszönhetően a programok bármilyen programozási nyelven olvashatják vagy írhatják. Az adatok ilyen formátumú tárolása azonban több lemezterületet, az interneten keresztüli átvitel során hálózati sávszélességet, valamint a kódoláshoz és dekódoláshoz szükséges processzoridőt igényel.

Egy másik lehetőség a bináris adatformátumok használata. Például, mielőtt az internet népszerűvé vált volna, az ISO-alapú hálózati platformot széleskörű hálózatépítésre használták. Az ISO-protokollok egy bináris formátumon, az ASN.1-en alapultak, amely a sokadik fokig szigorúan meghatározott volt, és bármilyen bináris adatkódolási igényt támogatni tudott. Az ASN.1 nagyrészt elfeledett dolog, kivéve valakit, mint én, aki harminc évvel ezelőtt azon dolgozott, hogy egy ISO-protokollverem implementációt hozzon a Unix rendszerekre. Ma ez egy történelmi példa a bináris adatformátumra, amely garantált hordozhatóságot biztosít a különböző programozási nyelveken írt alkalmazások között.

A Google ezt mondja a protokollpufferekről:

A protokollpufferek nyelvsemleges, platformsemleges, bővíthető mechanizmust biztosítanak a strukturált adatok előre és visszafelé kompatibilis sorosítására. Olyan, mint a JSON, kivéve, hogy kisebb és gyorsabb, és anyanyelvi kötéseket hoz létre.

A nyelvsemleges kifejezés azt jelenti, hogy a protokollpufferek kötései állnak rendelkezésre a legnépszerűbb programozási nyelvekhez, a platformneutral pedig azt jelenti, hogy a kötések több chip architektúrához is elérhetők. A strukturált adatok kifejezés azt jelenti, hogy a protokollpufferek támogatják a sémadeklaráció szerint kódolt adatokat, és hogy a séma támogatja a definíciók egymásba ágyazását más definíciókon belül.

A protokollpufferek használata a séma .proto fájlokkal történő leírásával kezdődik. Ezek a fájlok lehetővé teszik egy bináris adatblokk formátumának leírását, amelyet bináris üzenetpufferbe kívánnak kódolni. A .proto fájlok kódokká fordíthatók a közel tucatnyi programozási nyelv bármelyikén. A lefordított modul kódot ad az adatok bináris formátumba való kódolásához és a bináris formátumból történő dekódoláshoz. Az Önön múlik, hogy az alkalmazás mit kezd a kódolt adatokkal.

A tervek szerint a protokollpufferként kódolt adatokat egy hálózati protokollban használják az adatok interneten keresztüli kommunikálására, innen ered a projekt neve. A Google széles körben használ protokollpuffereket például belső alkalmazásokban. De ne hagyja, hogy ez a szándékolt használat korlátozza a képzeletét.

A hivatalos dokumentáció itt található: https://developers.google.com/protocol-buffers

A cikkben látható kód a következő címen érhető el: https://github.com/robogeek/nodejs-protocol-buffers

A Node.js protokollpufferei használatának megkezdése

Ezt a Google által kifejlesztett protokollpufferek használatával kezdjük.

Az első szükség az, hogy beszerezzük a protoc fordítót, amely a protokoll puffer definícióit kóddá konvertálja. Lehet, hogy szerencséd van, és a számítógéped csomagkezelője rendelkezik egy protokollpuffer-eszközöket tartalmazó csomaggal.

Például a macOS laptopomon MacPortokat használok nyílt forráskódú eszközök biztosítására. A protobuf3-cpp csomag viszonylag naprakész, és azt állítja, hogy tartalmazza a fordítót.

Ubuntuban azt találtam, hogy az protobuf-compiler tartalmazza a megfelelő eszközöket. Ezért az Ubuntu telepítése ilyen egyszerű:

$ sudo apt-get install protobuf-compiler

Ennek hiányában a https://github.com/protocolbuffers/protobuf/releases oldalra léphet a Protocol Buffers csapat által biztosított előre elkészített csomagok lekéréséhez. Vagy letöltheti a forráskódot, és saját maga is lefordíthatja.

Ennek a gyakorlatnak a célja egy protoc nevű parancssori eszköz, amely a protokollpufferek fordítója.

$ protoc
Usage: protoc [OPTION] PROTO_FILES 
Parse PROTO_FILES and generate output based on the options given:
...

A használati üzenet akkor kerül kinyomtatásra, ha a parancs argumentumok nélkül fut le.

A Protocol Buffers specifikációs nyelvnek két változata létezik. Ebben az oktatóanyagban ennek a nyelvnek a 3-as verzióját fogjuk használni, a dokumentáció pedig on-line elérhető: https://developers.google.com/protocol-buffers/docs/proto3

A webhely sajnos nem tartalmaz oktatóanyagot a Node.js használatához. Megnézheti más nyelvek hivatalos oktatóanyagait.

A Google biztosít egy Node.js csomagot a következő címen: https://www.npmjs.com/package/google-protobuf

A csomag forrástárolója pedig nagyjából egy oktatóanyaghoz hasonló dokumentációt tartalmaz: https://github.com/protocolbuffers/protobuf-javascript/blob/main/docs/index.md

Egyszerű adatformátum meghatározása Protocol Buffers segítségével

Az oktatóanyag hátralévő részében egy egyszerű adatformátumon megyünk át, amelyet protokollpufferek és egy pár Node.js szkript határoz meg az adatok kódolásához és dekódolásához az adatformátum használatával. Ez egy egyszerű Todo objektum lesz, hasonló ahhoz, amit egy minta alkalmazásban implementáltam, amelyet az akkor újonnan kiadott Bootstrap v5 felfedezésére írtam.

Lásd: https://techsparx.com/nodejs/examples/todo-bootstrap/

Hozzon létre egy projektkönyvtárat:

$ mkdir protobuf
$ cd protobuf
$ npm init -y
$ npm install google-protobuf --save

Ehhez telepíteni kell egy külső függőséget, a protoc fordítóprogramot, amelyet az előző részben ismertettünk.

A könyvtárban hozzon létre egy todo.proto nevű fájlt, és tegye ezt a fájl tetejére:

syntax = "proto3";

Ez arra utasítja a protoc-et, hogy használja a harmadik verziót. Ha ezt nem teszi meg, a fordító egy figyelmeztetést nyomtat, amely a Nincs szintaxis megadva a protofájlhoz szöveget, és felszólítja, hogy a fenti szöveget adja hozzá a fájlhoz.

A példaalkalmazásomban a TODO objektum négy mezőt tartalmaz:

  1. ID, amely az objektumot azonosító szám
  2. Egy title karakterlánc, amely a TODO listákban jelenik meg
  3. Egy body karakterlánc, amely akkor jelenik meg, amikor a felhasználó az egyetlen TODO elemet nézi
  4. Elsőbbségi felsorolás, amely magas/közepes/alacsony prioritást ad

A protokoll pufferekben a definíció így néz ki:

message Todo {
    int64 id = 1;
    string title = 2;
    string body = 3;
    Precedence precedence = 4;
}

A message szó elindítja egy objektum meghatározását. A message blokknak van egy neve, ebben az esetben Todo, amely megadja az osztály nevét. A törzsben egy vagy több meződefiníció található. Minden mezőnek van adattípusa és neve. A szám-hozzárendelés a mező száma, nem pedig az alapértelmezett érték. A mező száma határozza meg, hogy az adatok hol vannak kódolva egy rekordon belül.

Az utolsó mező a Precedence típusú. Ez nincs beépítve a protokollpufferek nyelvébe, de ez az alkalmazás határozza meg:

enum Precedence {
     PRECEDENCE_NONE = 0;
     PRECEDENCE_LOW = 1;
     PRECEDENCE_MEDIUM = 2;
     PRECEDENCE_HIGH = 3; 
}

A enum objektumtípust választottuk a precedence mező megengedett értékeinek leírására. A példaalkalmazásomban az ALACSONY, KÖZEPES és MAGAS értékeket az itt látható módon határoztuk meg. Ez azt jelentette, hogy a enum eredetileg csak ezzel a három értékkel volt megadva, és a PRECEDENCE_NONE nem volt ott. De a fordító ezt a hibát adta:

The first enum value must be zero in proto3.

A enum első mezőjének 0 értékkel kell rendelkeznie. A korábbi Todo alkalmazásban használt értékekkel (1, 2, 3) való kompatibilitás fenntartása érdekében ugyanazokat az értékeket szerettem volna megtartani. Ez a PRECEDENCE_NONE 0 értékű bevezetését jelentette.

A protokoll pufferek dokumentációjából kitűnik, hogy az enum deklarációk elhelyezhetők a message Todo {...} törzsében, de a minta implementációban ez elkülönítve van.

Ez egy szinguláris objektumot, vagy úgynevezett skaláris értéktípust határoz meg. Egy tucatnyi adattípus létezik, amelyek egész számokra, lebegőpontokra, logikai értékekre és karakterláncokra vezethetők vissza. Mindegyik pontosan úgy van definiálva, hogy N bájtot vegyen fel, hogy helyesen binárisként kódolható legyen.

Egy másik megfontolandó kérdés a mezők száma. Egy alkalmazás fejlődése során előfordulhat, hogy módosítania kell az objektumdefiníciót. Felhasználhat egy meglévő mezőt egy másik típusú érték tárolására. Ez azonban tönkreteszi azokat az alkalmazásokat, amelyeket már telepített a területen. Ehelyett a legjobb gyakorlat a régebbi meződefiníciók helybenhagyása, vagy a reserved kulcsszó használata a régebbi mezők kizárására. A cél az alkalmazás régebbi kiadásaival való visszamenőleges kompatibilitás fenntartása.

Például használhatjuk a Markdownt a body mezőben. A meglévő body mező egyszerű szöveget, nem Markdownt tartalmaz. Módosíthatjuk az üzenet definícióját a következőre:

message Todo {
    int64 id = 1;
    string title = 2;
    string bodyMD = 5;
    Precedence precedence = 4;
    reserved 3;
}

Ez a módosítás átnevezi a body mezőt bodyMD-re, hogy egyértelmű legyen, hogy ez a mező tárolja a Markdownt. Ez az új mező 5, a 3 mező száma pedig reserved. Alternatív megoldásként hagyja a régi body meződefiníciót, de az alkalmazás figyelmen kívül hagyja ezt a mezőt.

Az olyan skaláris értéktípusok, mint a Todo, csak egy objektumhoz használhatók. Érdemes lehet elküldeni egy listát a Todo elemekről, ezért szükség van egy módra az objektumok tömbjének megadására.

A protokoll pufferekben ezt így írod le:

message Todos {
   repeated Todo todos = 1 
}

Meghatároztunk egy új objektumtípust, a Todos. A repeated szó most meghatározzuk, hogy mi a lényegében egy tömb. Ez azt jelenti, hogy az üzenet nulla vagy több példányt tartalmaz a Todo objektumból, és az 1-es mezőben van elhelyezve. Nagyjából ez egy tömbbé teszi. Ez az objektum könnyen tartalmazhat más elemeket is, ha ez szükséges az alkalmazásához.

Node.js forrás előállítása a TODO protobuf sémához

Összeállítottunk egy teljes sémát. A következő lépés a forráskód átalakítása, amelyet egy alkalmazásban használhatunk. Ezért telepítettük korábban a protoc-et.

Ez a fordító több nyelvhez is képes forráskódot generálni. BTW, ha emlékszik vissza az informatika óráira, egy fordító lefordítja a forráskódot egy programozási nyelvről egy teljesen másik nyelvre. Ezért a protoc lefordítja a protokollpufferek forráskódját számos más programozási nyelv bármelyikére.

Ha elolvassa a Protocol Buffers dokumentációs webhelyet, akkor megvakarhatja a fejét – a Node.js-szel szeretnénk használni, de nincs dokumentáció a Node.js használatáról. Erre a Protocol Buffers csapatának meg kell válaszolnia, miért nem teszik közzé a Node.js dokumentációját a webhelyükön.

Nyissa meg a következőt: https://www.npmjs.com/package/google-protobuf

Ezután kövesse néhány linket a következő helyre való eljutáshoz: https://github.com/protocolbuffers/protobuf-javascript/blob/main/docs/index.md

Ez a két oldal a Google által készített dokumentációt tartalmazza a protokollpufferek Google implementációjának használatáról, beleértve a protoc használatát a Node.js (JavaScript) kód generálására. Elgondolkodtató, hogy a Google miért nem terjeszti ki ezt úgy, hogy a fő webhelyen is tartalmazza a dokumentációt. Ennek oka lehet a csomag dokumentációjában szereplő figyelmeztetések, mivel a projekt állapota 2022 júliusában az, hogy a projekt némileg megszakadt, amit megpróbálnak kijavítani.

A package.json mezőben adja hozzá ezt a script bejegyzést:

"scripts": {
    "protoc": "protoc --js_out=import_style=commonjs,binary:. todo.proto"
},

A legjobb gyakorlat, ha az ehhez hasonló parancsokat belefoglalja ebbe a fájlba, hogy ne kelljen értékes agysejteket költenie apróságokra.

A JavaScript kimenethez két stílus létezik. Az egyik a CommonJS használatával történő importálás támogatása a require függvénnyel, ahogy a Node.js hagyományos használata, a másik pedig a Google Closure stílusú importálás. Mivel a Node.js-t célozzuk meg, commonjs értéket adunk meg. Az binary opció hatására a függvények binárisan szerializálódnak, és binárisból deszerializálódnak. Az opció :. része a kimeneti könyvtárat adja meg, ebben az esetben az aktuális könyvtárat. Az :build/gen használata azt jelenti, hogy a kimeneti könyvtár ./build/gen. A parancs utolsó része meghatározza a bemeneti fájlt vagy fájlokat, ebben az esetben todo.proto.

A fordító jól működik a hibaüzenetekkel. Néhányat korábban bemutattak, és könnyű volt meghatározni, mit kell tenni.

Ez a parancs egy todo_pb.js fájlt hoz létre az aktuális könyvtárban. Tanulságos elolvasni ezt a fájlt. Látni fogja, hogy JavaScript objektumdefiníciók jönnek létre a Todo, Todos és Precedence számára.

A tetején ez áll:

var jspb = require('google-protobuf');

A mi kódunk nem használja a google-protobuf-t, hanem a generált kód. A csomagban található függvények liberális használatát a generált kódon keresztül láthatja. A csomag elérhetőségének biztosítása érdekében korábban telepítettük.

Adatok kódolása protokollpufferekhez

Egy valós alkalmazásban előfordulhat, hogy van egy kéréskezelő funkciónk, amely összegyűjt néhány adatot, és a válasz küldéséhez protokollpufferekkel kell formázni. Esetünkben szeretnénk bemutatni azt a fő lépést, hogy előállítunk egy protokollpuffer objektumot, majd sorba állítjuk egy bináris fájlba. A következő szkriptben bemutatjuk a bináris fájl deszerializálását az adatok olvasásához.

Hozzon létre egy encode.mjs nevű fájlt (mivel az ES6 modulok jelentik a JavaScript jövőjét, lehetőség szerint ezeket használjuk). Kezdje ezzel:

import { default as Schema } from './todo_pb.js';
import { promises as fsp } from 'fs';

// console.log(Schema);

const todos = new Schema.Todos();

let todo = new Schema.Todo();
todo.setId(1);
todo.setTitle("Buy cheese");
todo.setBody("PIZZA NIGHT");
todo.setPrecedence(Schema.Precedence.PRECEDENCE_HIGH);
todos.addTodos(todo);

A generált kód CommonJS formátumú, és úgy tűnik, nincs lehetőség ES6 modul létrehozására. Ezt az importálási mintát tartották a leghasznosabbnak. A fs/promises-et fsp-ként is importáljuk, így aszinkron fájlrendszer-funkcióink vannak.

A generált kód lehetővé teszi a new Schema.Todos() és new Schema.Todo() használatát a megfelelő objektumok előállításához.

A Todo objektumhoz nem találtam remek módot a mezőértékek beállítására. Ehelyett konkrétan meg kell hívnunk a set metódusokat, amint az itt látható. A Todo objektum létrehozása után adja hozzá a Todos objektumhoz a todos.addTodos használatával.

Ismételje meg a kód utolsó bitjét, ahányszor csak akarja, tetszés szerint módosítva az értékeket. Fejezd be a szkriptet ezzel:

console.log(todos.toObject());

await fsp.writeFile('todos.bin', todos.serializeBinary());

Az első vizuális visszajelzést ad az Ön által létrehozott objektumról. A toObject metódus a protokoll pufferek objektumát normál JavaScript objektummá alakítja.

Az utolsó sor egy fájlba írja az adatokat, todos.bin. A serializeBinary metódus az objektumot bináris blobbá alakítja, amely ezután a fájlba kerül.

A kimenet valahogy így fog kinézni:

{
  todosList: [
    { id: 1, title: 'Buy cheese', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 2, title: 'Buy sauce', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 3, title: 'Buy Spinach', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 4, title: 'Buy ham', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 5, title: 'Buy olives', body: 'PIZZA NIGHT', precedence: 3 }
  ]
}

Nálunk minden szombat este a nulláról készítjük a pizzát.

Megvizsgálhatjuk a bináris fájlt is:

$ od -c todos.bin 
0000000  \n 035  \b 001 022  \n   B   u   y       c   h   e   e   s   e
0000020 032  \v   P   I   Z   Z   A       N   I   G   H   T     003  \n
0000040 034  \b 002 022  \t   B   u   y       s   a   u   c   e 032  \v
0000060   P   I   Z   Z   A       N   I   G   H   T     003  \n 036  \b
0000100 003 022  \v   B   u   y       S   p   i   n   a   c   h 032  \v
0000120   P   I   Z   Z   A       N   I   G   H   T     003  \n 032  \b
0000140 004 022  \a   B   u   y       h   a   m 032  \v   P   I   Z   Z
0000160   A       N   I   G   H   T     003  \n 035  \b 005 022  \n   B
0000200   u   y       o   l   i   v   e   s 032  \v   P   I   Z   Z   A
0000220       N   I   G   H   T     003
0000230

Nézze meg alaposan a bájtokat. Az egyes címek szövege előtt mezőszámnak tűnik, és minden egyes karakterlánc hosszának tűnik, és így tovább. Ha szeretné, a dokumentáció tartalmazza ennek a formátumnak a részletes leírását. Ezen a ponton fontos észrevenni, hogy adataink ebben a fájlban vannak.

Egy másik dolog, amit észre kell venni, a relatív méretkülönbség. A szöveges forma sokkal több bájtot vesz fel, mint a bináris forma.

A protokollpufferek deszerializálása a Node.js használatával

Mivel a protokollpufferek nyelvsemlegesek, az adatokat deszerializálhatjuk egy másik nyelven írt kód használatával. De ez arról szól, hogy ezt a Node.js-ben tegyük, ezért koncentráljunk erre.

Hozzon létre egy decode.mjs nevű fájlt, amely tartalmazza:

import { default as Schema } from './todo_pb.js';
import { promises as fsp } from 'fs';

const todosBin = await fsp.readFile('todos.bin');
const todos = Schema.Todos.deserializeBinary(todosBin);

console.log(todos);
console.log(todos.toObject());

Ez egyszerűen beolvassa a todos.bin értéket, és az adatokat egy objektummá deszerializálja. Ezután kinyomtatjuk magát az objektumot és a toObject űrlapot is.

{
  wrappers_: { '1': [ [Object], [Object], [Object], [Object], [Object] ] },
  messageId_: undefined,
  arrayIndexOffset_: -1,
  array: [ [ [Array], [Array], [Array], [Array], [Array] ] ],
  pivot_: 1.7976931348623157e+308,
  convertedPrimitiveFields_: {}
}
{
  todosList: [
    { id: 1, title: 'Buy cheese', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 2, title: 'Buy sauce', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 3, title: 'Buy Spinach', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 4, title: 'Buy ham', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 5, title: 'Buy olives', body: 'PIZZA NIGHT', precedence: 3 }
  ]
}

Az első bepillantást enged a protokollpuffer objektumok fedelébe. Nem kell belemélyednünk a részletekbe, de érdekes ezt látni. A második ugyanaz az adat, amit fent láttunk, jelezve, hogy sikeresen átvittük az adatokat egyik alkalmazásból a másikba.

Adatátvitel az alkalmazások között protokollpufferek használatával

Magas szinten a folyamatot a következőképpen látjuk:

  1. Határozza meg a sémát .proto fájlokban, majd állítson elő kódot az összes érdeklődő nyelvhez
  2. Az adatok átvitele egy protokoll puffer üzenetobjektum generálásával kezdődik, majd a serializeBinary metódus meghívásával kezdődik
  3. Az adatok fogadása a deserializeBinary metódus meghívásával történik, az üzenetpuffert protokoll puffer objektummá alakítja, majd ezeket az adatokat az alkalmazásban használja.

Alternatív protokoll pufferek megvalósítása a Node.js/JavaScript számára

A hivatalos Google protokoll pufferek megvalósítása hagy némi kívánnivalót maga után, ha a Node.js-szel használja. Az npm adattárat böngészve több más csomagot is találunk, amelyek lefedik ugyanazt a helyet. Az egyik a protocolbuf.js, amely pusztán JavaScript-implementációnak számít, TypeScript-támogatással, amely Node.js-en és böngészőkön is fut.

Két csomag van:

  1. A protobufjs futásidejű támogatást tartalmaz a protokollpufferek objektumok használatához, a sémák elemzéséhez és használatához, és még sok máshoz
  2. A protobufjs-cli egy parancssori eszköz, amely nagyjából egyenértékű a protoc-el

A dokumentáció (https://www.npmjs.com/package/protobufjs) két használati módról beszél:

  1. Töltse be a .proto fájlokat közvetlenül fordítás nélkül, és azonnal indítsa el a metódusok hívását az objektumokon
  2. Fordítsa le a .proto fájlokat statikus osztályokba, hasonlóan a fent láthatóhoz

Ebben az oktatóanyagban a második módot fogjuk használni, hogy könnyebb legyen kontrasztot adni az imént átsétált kóddal.

Telepítse a csomagokat a következő módon:

$ npm install protobufjs protobufjs-cli --save

Ez utóbbi két parancsot telepít, a pbjs és pbts, amelyek támogatják a JavaScript, illetve a TypeScript használatát.

Ha a .proto fájlt használható kódra szeretné fordítani, futtassa a következőt:

$ npx pbjs -t static-module -w commonjs -o dist-pbjs/todo.js todo.proto 
#### OR, for ES6 code generation 
$ npx pbjs -t static-module -w es6 -o dist-pbjs/todo-es6.mjs todo.proto

Ez egy „statikus modul” létrehozását célozza meg, vagyis forráskód létrehozását. A modul formátuma CommonJS vagy ES6 formátum lesz, az Ön preferenciáitól függően. Vegye figyelembe, hogy az ES6 modult a .mjs kiterjesztéssel neveztük el a Node.js kompatibilitás érdekében.

Hasznos megvizsgálni a generált kódot, már csak azért is, mert ez a legpraktikusabb módja a generált API megtanulásának. Hiányosnak találtam a projektdokumentációt, és a generált kód elég tiszta volt ahhoz, hogy közvetlenül megértsem a csomag használatát.

import { default as Schema } from './dist-pbjs/todo.js';
import { promises as fsp } from 'fs';

const todos = new Schema.Todos();

todos.todos.push(new Schema.Todo({
    id: 1,
    title: "Buy Cheese",
    body: "PIZZA NIGHT",
    precedence: Schema.Precedence.PRECEDENCE_HIGH
}));

// ...

console.log(Schema.Todos.toObject(todos));

await fsp.writeFile('todos-protobufjs.bin', Schema.Todos.encode(todos).finish());

Ennél a csomagnál kicsit más a használat. Például példányosíthatunk egy objektumpéldányt egy tulajdonság objektum használatával. A forráskód tanulmányozása során azt látjuk, hogy olyan tulajdonságok jönnek létre, amelyekhez közvetlenül tudunk értékeket rendelni.

A toObject és encode metódusok nincsenek az objektumpéldányhoz csatolva, hanem az osztály statikus metódusai. Ezért a Schema.Todos.toObject-at hívjuk todos.toObject helyett.

Futtassa az alkalmazást, és ezt a kimenetet látjuk a toObject reprezentációhoz:

{
  todos: [
    { id: 1, title: 'Buy Cheese', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 2, title: 'Buy sauce', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 3, title: 'Buy Spinach', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 4, title: 'Buy ham', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 5, title: 'Buy olives', body: 'PIZZA NIGHT', precedence: 3 }
  ]
}

Ez nagyjából ugyanaz, mint az előző. A kimeneti fájl pontosan ugyanolyan méretű, mint az előző példában:

$ ls -l todos* 
-rw-rw-r-- 1 david david 152 Aug 22 11:38 todos.bin 
-rw-rw-r-- 1 david david 152 Aug 22 11:37 todos-protobufjs.bin

Ez igazolja azt az elképzelést, hogy a protokollpufferek nyelvsemlegesek, mivel két különböző protokollpuffer implementációt használtunk ugyanazon fájl létrehozásához.

A decode.mjs kis módosításával a parancssorban elnevezhetjük a fájlt, majd a következő módon dekódolhatjuk az adatfájlt:

$ node decode.mjs todos-protobufjs.bin 
{
  wrappers_: { '1': [ [Object], [Object], [Object], [Object], [Object] ] },
  messageId_: undefined,
  arrayIndexOffset_: -1,
  array: [ [ [Array], [Array], [Array], [Array], [Array] ] ],
  pivot_: 1.7976931348623157e+308,
  convertedPrimitiveFields_: {}
}
{
  todosList: [
    { id: 1, title: 'Buy Cheese', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 2, title: 'Buy sauce', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 3, title: 'Buy Spinach', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 4, title: 'Buy ham', body: 'PIZZA NIGHT', precedence: 3 },
    { id: 5, title: 'Buy olives', body: 'PIZZA NIGHT', precedence: 3 }
  ]
}

Ez bemutatja egy protokoll pufferfájl létrehozását az egyik implementációval, és dekódolását egy másik megvalósítással.

A protobufjs-szal írt dekóder így néz ki:

import { default as Schema } from './dist-pbjs/todo.js';
import { promises as fsp } from 'fs';

const todosBin = await fsp.readFile(process.argv[2]);

const todos = Schema.Todos.decode(todosBin);

console.log(Schema.Todos.toObject(todos).todos);

A végrehajtás (node decode-protobufjs.mjs todos-protobufjs.bin) így néz ki:

[
  {
    id: Long { low: 1, high: 0, unsigned: false },
    title: 'Buy Cheese',
    body: 'PIZZA NIGHT',
    precedence: 3
  },
  {
    id: Long { low: 2, high: 0, unsigned: false },
    title: 'Buy sauce',
    body: 'PIZZA NIGHT',
    precedence: 3
  },
  {
    id: Long { low: 3, high: 0, unsigned: false },
    title: 'Buy Spinach',
    body: 'PIZZA NIGHT',
    precedence: 3
  },
  {
    id: Long { low: 4, high: 0, unsigned: false },
    title: 'Buy ham',
    body: 'PIZZA NIGHT',
    precedence: 3
  },
  {
    id: Long { low: 5, high: 0, unsigned: false },
    title: 'Buy olives',
    body: 'PIZZA NIGHT',
    precedence: 3
  }
]

Érdekes módon a id mező másképp van ábrázolva. Ehelyett egy olyan objektum, amely alkalmasnak tűnik egy numerikus tartomány ábrázolására. Ellenkező esetben a kimenet ugyanaz, mint az előző megvalósításnál.

Benchmarking kódolás és dekódolás JSON, Protocol Buffers és Protobuf.JS használatával

Létrehoztam három benchmark függvénypárt, amelyek kizárólag a kódolási vagy dekódolási funkciót hívják meg az előre létrehozott objektumokon. JSON esetén a JSON.stringify és JSON.parse, a többi pedig a fent látható funkciókat használja. Az objektumtömb ebben az esetben 1000 elemből áll.

$ node bench.mjs 
cpu: Intel(R) Core(TM) i7-5600U CPU @ 2.60GHz
runtime: node v18.6.0 (x64-linux)

benchmark        time (avg)             (min … max)
---------------------------------------------------
encode-JSON  342.37 µs/iter   (311.93 µs … 1.19 ms)
decode-JSON   435.9 µs/iter   (384.44 µs … 1.41 ms)
encode-PB    946.43 µs/iter   (777.38 µs … 3.13 ms)
decode-PB    770.79 µs/iter   (688.99 µs … 1.78 ms)
encode-PBJS  696.75 µs/iter   (618.43 µs … 2.43 ms)
decode-PBJS  455.36 µs/iter   (413.66 µs … 1.09 ms)

Érdekes, hogy a JSON kódolás és dekódolás lényegesen gyorsabb, mint a protokollpufferek megfelelője.

Egy másik mérőszám az egyes ábrázolások mérete:

-rw-rw-r-- 1 david david 328 Aug 22 16:46 todos.json 
-rw-rw-r-- 1 david david 152 Aug 22 11:38 todos.bin 
-rw-rw-r-- 1 david david 152 Aug 22 12:18 todos-protobufjs.bin

Az egyenértékű JSON több mint kétszer akkora. Nyilvánvaló, hogy a szöveg alapú adatformátum nagyobb lesz, mint egy bináris adatformátum.

Összegzés

A Google protokollpufferei hatékony módot kínálnak az alkalmazások közötti adatcserére, vagy egy alkalmazás számára az adatok tárolására. Az adatstruktúrákat kompakt bináris adatblobokként kódolja.

A protokollpufferek elsődleges előnye a kódolt adatok mérete. Mivel jól meghatározott, és több programozási nyelven is elérhető, számos alkalmazáshoz jól illeszkedik.

A szöveges adatformátumok is jól meghatározottak, és több programozási nyelven is elérhetők. Nyilvánvalóan a JSON-t, YAML-t, XML-t és hasonlókat használó alkalmazások nagy száma az adatátvitelhez bizonyítja ezek hasznosságát. De mi a helyzet azokkal az esetekkel, amikor fontos a hálózati sávszélesség megőrzése? Legyen szó néhány százezer elfoglalt szervert befogadó szerverfarmról, távolról telepített IoT-eszközről, amely 5G mobiladat-kapcsolaton keresztül csatlakozik, vagy egy okostelefon-alkalmazásról, számos forgatókönyv létezik, ahol a kompakt bináris adatok javítják a teljesítményt vagy csökkentik a hálózati adatköltségeket.

A szerzőről

„David Herron”: David Herron író és szoftvermérnök, aki a technológia bölcs használatára összpontosít. Különösen érdeklik a tiszta energiatechnológiák, mint a napenergia, a szélenergia és az elektromos autók. David közel 30 évig dolgozott a Szilícium-völgyben szoftvereken, az elektronikus levelezőrendszerektől a videó streamingen át a Java programozási nyelvig, és számos könyvet publikált a Node.js programozásról és az elektromos járművekről.

Eredetileg a https://techsparx.com címen tették közzé