Protocol Buffers in C# dotnet core

Geschreven door Stefan van Tilborg op

Veel applicaties wisselen data uit onderling. Zeker in de tegenwoordig populaire microservice architectuur gebaseerde gedistribueerde applicaties. Toen ik begon in de ICT als ontwikkelaar was XML de standaard in berichtformaten. Het is leesbaar en gestructureerd, en er zijn een grote verscheidenheid aan tools en technieken (denk aan XSD, XSLT ed.) voor beschikbaar. XML heeft wel een groot nadeel; het voegt bij het serializeren van objecten een hoop overhead toe.

Laten we het volgende persoon object als voorbeeld nemen:

class Persoon
{
    public string Voornaam { get; set; }
    public string Achternaam { get; set; }
    public int Leeftijd { get; set; }
}

Als bovenstaande geserialiseerd wordt naar XML krijg je de volgende output:

<?xml version="1.0" encoding="utf-16"?>
<Persoon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Voornaam>Stefan</Voornaam>
    <Achternaam>Achternaam</Achternaam>
    <Leeftijd>38</Leeftijd>
</Persoon>"

Zoals je ziet heb je zowel een start als een sluit tag om elk stukje data heen, vooral met grotere objecten kan dit snel groeien in grootte.

Met de komst van JSON kwam een verbetering in de grootte van de output van de geserialiseerde objecten. Onze persoon class geeft met de Newtonsoft JSON serialiser de volgende output:

{
    "Voornaam":"Stefan",
    "Achternaam":"Achternaam",
    "Leeftijd":38
}

Dit is al een stuk beter, zeker wanneer er high-volume data wordt uitgewisseld kan er veel winst geboekt worden door over te schakelen van XML naar JSON. Maar JSON heeft nog steeds een groot nadeel. In elk geserialiseerd object staat de veld naam uitgeschreven. Dit wordt voor elk te verzenden object over de lijn meegestuurd.

Protocol Buffers is een techniek ontwikkeld door Google als formaat van data uitwisseling tussen interne applicaties en services. De eerste versie stamt uit 2008 en wordt als default bericht formaat voor gRPC. De huidige versie van Protocol Buffers is versie 3 en deze versie gebruik ik in het verdere verloop van dit artikel. Protocol Buffers heeft 2 grote voordelen over XML en JSON:

  • Bericht grootte
  • Snelheid van serialiseren

Allereerst de bericht grootte; laten we het persoon object ook omzetten naar het Protocol Buffers formaat (de output in UTF8):

"\n\u0006Stefan\u0012\vvan Tilborg\u0018&"

Het is niet goed leesbaar meer vanwege het binaire formaat dat Protocol Buffers gebruikt, maar dit kan na verzenden weer gedeserialiseerd worden in de oorspronkelijke class.

Dit formaat is als volgt opgebouwd:

{field_number} + {field_type} + {data}

Protocol Buffers kan de data zo klein houden omdat de data en de message structuur van elkaar gescheiden zijn. Verder op in dit artikel laat ik zien hoe.

Het 2e voordeel is de snelheid van serialiseren, hiervoor heb ik een demo app gemaakt in .net core 3.0 welke hier te downloaden is:
https://github.com/stefanvt1981/ProtoBufDemo

In dit voobeeld laat ik een class 1000 keer serialiseren en de-serialiseren in zowel XML, JSON als Protocol Buffers, voor elke serialiseer actie start ik een stopwatch en na afloop van de 1000 keer bepaal ik de looptijd. Na runnen krijg je deze output:

alt text

XML en JSON ontlopen elkaar niet zoveel in snelheid, maar in grootte is JSON al snel de helft kleiner, maar dit is nog niets vergeleken met de Protocol Buffers welke wel 7 keer kleiner is dan XML en wel 30 keer sneller kan serialiseren!

Dit is uiteraard mooi in theorie, maar hoe werkt het precies? Om te beginnen maken we een nieuwe console app.

alt text

Hierin hebben we 2 nuget packages nodig:

  • Google.ProtoBuf
  • Google.ProtoBuf.Tools

De eerste bevat de classes die nodig zijn om te kunnen serialiseren vanuit de applicatie. De 2e bevat de tool om protobuf c# classes te kunnen genereren; de protoc compiler.

Zoals ik eerder al vertelde is in Protocol Buffers de implementatie van de data gescheiden, dit gebeurd middels de proto file. De proto file voor onze persoon class, PersoonPB.proto , ziet er als volgt uit. Voeg deze file toe aan de console app.

syntax = "proto3";
 
option csharp_namespace = "ProtoDemo";
 
message PersoonPB {
    string voornaam = 1;
    string achternaam = 2;
    int32 leeftijd = 3;
}

Het keyword syntax geeft de versie van de Protocol Buffers aan, in dit geval versie 3. Met “option csharp_namespace” kun je de namespace van de gegenereerde classes die de serialisation regelen beïnvloeden. Met het keyword message definieer je de berichten. Deze kunnen bestaan uit simple types als string en int, maar ook uit geneste messages. Alle overige data types kun je vinden op de proto3 reference:
https://developers.google.com/protocol-buffers/docs/proto3

message AdresPB {
    string straat = 1;
    string huisnummer = 2;
}
message PersoonPB {
    string voornaam = 1;
    string achternaam = 2;
    int32 leeftijd = 3;
    AdresPB adres = 4;
}

Nu de proto file gedefineerd is kan deze gecompileerd worden. In de nuget package Google.ProtoBuf.Tools zit de file protoc.exe. Gebruik deze om de proto file te compileren met het volgende commando:

protoc --csharp_output=. PersoonPB.proto

–csharp_output geeft de compiler aan een c# class aan te maken, de . geeft aan deze in de huidige directory te plaatsen. Met protoc –help kunnen alle opties bekeken worden.

De gegenereerde class wordt automatisch aan het project toegevoegd. alt text Het console project met de gegenereerde PersoonPB.cs class

Nu we de gecompileerde PersoonPB.cs file hebben kunnen we beginnen met serialiseren.

class Program
{
    static void Main(string[] args)
    {
        // Protocol Buffers
        var persoonPB = new PersoonPB
        {
            Voornaam = "Stefan",
            Achternaam = "van Tilborg",
            Leeftijd = 38
        };
 
        // Serialiseer
        using (var output = File.Create("persoon.dat"))
        {
            persoonPB.WriteTo(output);
        }
 
        // De-Serialiseer
        using (var input = File.OpenRead("persoon.dat"))
        {
            var persoonDeserialized =    
                            PersoonPB.Parser.ParseFrom(input);
        }
    }
}

De protoc compiler heeft de gegenereerde class voorzien van een methode die nodig is om te kunnen serializen: void WriteTo, welke naar een stream kan schrijven. Standaard werkt deze met een CodedOutputStream class, maar in de class Google.ProtoBuf.MessageExtentions staan methodes die het mogelijk maken om met andere streams te werken. De-serialeren verloopt via de Google.ProtoBuf.MessageParser class. Deze heeft de methode ParseFrom() welke een stream of byte[] als parameter heeft. Stap de applicatie eens door met een breakpoint in Debug modes, dan kun je de werking van de serialisatie goed volgen.

Tot slot de voor en nadelen op een rij:

VoordelenNadelen
Snelle serialisatie in vergelijking met JSON en XMLGeserialiseerde berichten zijn minder makkelijk leesbaar
Kleine berichten door scheiding van data en structuur
Snellere gegevens overdracht door kleinere berichten

Voor een applicatie waar veel data uitwisseling plaatsvind zullen de voordelen zeker opwegen tegen het nadeel.

Tot zover deze overview van Protocol Buffers in C#, in mijn volgende post ga ik blijf ik in de Google sfeer en ga in op gRPC in dotnet core. Dankjewel voor het lezen en als je vragen hebt of meer informatie wil kijk dan op de sites hieronder of stuur me een mailtje.

Voor meer informatie over Protocol Buffers kun je terecht op de volgende sites:

Algemene informatie:
https://developers.google.com/protocol-buffers

CSharp specifiek:
https://developers.google.com/protocol-buffers/docs/csharptutorial

MSDN:
https://docs.microsoft.com/en-us/aspnet/core/grpc/dotnet-grpc?view=aspnetcore-3.0

Alle code voorbeelden staan op mijn GitHub:
https://github.com/stefanvt1981/ProtoBufDemo

← Terug
XPRTZ