Protocol buffers (protobuf) to rozwijany przez Google, niezależny od języka programowania i platformy, rozszerzalny sposób na binarną serializację strukturalnych danych.
Z powodzeniem można go przedstawić jako jedną z alternatyw dla XML’a. Pracę z biblioteką rozpoczynamy od określenia struktury danych oraz utworzenia na jej podstawie kodu źródłowego klas w wybranych języku programowania. Wygenerowany kod służy do przechowywania tych danych oraz ich obsługi.
Spis treści
Geneza powstania
Google w ramach swoich systemów przesyła dane w tysiącach różnych formatów. Dotychczasowo znane formaty, jak np. XML nie spisywały się dobrze w tak dużej skali ze względu na swoje ograniczenia i narzuty związane z wydajnością. Dzięki Procol Buffers znacząca część tych problemów została rozwiązana.
Obecna wersja proto3 wspiera większość popularnych języków: Java, C++, Python, Java Lite, Ruby, JavaScript, Objective-C, C# oraz PHP.
Od roku 2008 biblioteka jest dostępna publicznie na zasadach wolnej licencji BSD.
Wprowadzenie do technologii
Technologia opiera się na definicjach komunikatów w specjalnym języku pośrednim, który jest przechowywany w plikach .proto. Poszczególne komunikaty protocol buffer to mała jednostka informacji zawierająca pary nazwa-wartość.
Poniżej plik .proto z definicją użytkownika oraz przykładowe wykorzystanie wygenerowanej klasy.
package stormit; option java_package = "pl.stormit.protobuf"; option java_outer_classname = "UserProtos"; message User { required string name = 1; optional string surname = 2; required int32 age = 3; enum UserType { NORMAL = 0; ADMIN = 1; } message Interest { required string name = 1; } repeated Interest interests = 4; required UserType userType = 5; }
UserProtos.User.Builder userBuilder = UserProtos.User.newBuilder(); UserProtos.User.Interest.Builder interestBuilder = UserProtos.User.Interest.newBuilder(); userBuilder.setAge(100).setName("Tomasz") .setUserType(UserProtos.User.UserType.ADMIN); userBuilder.addInterests(interestBuilder.setName("Java").build()) .addInterests(interestBuilder.setName("Protobuf").build()); UserProtos.User user = userBuilder.build();
Użytkownik ma zdefiniowane 3 typy proste: imię, nazwisko oraz wiek. Dodatkowo zdefiniowany jest typ użytkownika w postaci typu wyliczeniowego. Jest również podana lista zainteresowań (repeated Interest interests = 4;).
Składnia na pierwszy rzut oka wygląda na podobną do Javy, więc podstawowe funkcjonalności nie powinny sprawić większej trudności. Typ UserType oraz Interest mogłyby być zdefiniowane poza typem głównym User, wtedy byłyby zmapowane na osobne klasy, a nie klasy zagnieżdżone.
Kolejną rzeczą, która wymaga wyjaśnienia są przypisane wartości w definicji komunikatu (” = 1″, ” = 2″). Nie są to jak w Javie domyślne wartości, a unikatowe tagi wykorzystywane przy binarnej serializacji. Na podstawie tego znacznika pole jest rozpoznawane podczas serializacji i deserializacji, dlatego po jego ustawieniu nie powinno być już zmieniane. W celach optymalizacyjnych do często występujących elementów (np. repeated) należy korzystać z tagów z przedziału 1-15, ponieważ do zapisania tych liczb potrzeba najmniej pamięci.
Na podstawie utworzonych definicji komunikatów podczas kompilacji powstają docelowe klasy.
Zalecenia odnośnie rozszerzania i modyfikacji plików proto
Komunikatu Protobuf są wstecznie kompatybilne, trzeba jednak przestrzegać pewnych reguł podczas ich modyfikacji:
- nie wolno zmieniać raz przypisanych wartości tagów dla istniejących już pól
- nie wolno dodawać ani usuwać wymaganych pól
- dla nowo dodanych pól muszą zostać wykorzystane nowe, nieużywane wcześniej wartości tagów
Przestrzegając powyższych zaleceń, jedyne co trzeba zrobić, to na nowo przekompilować pliki .proto do Javy. Starsze klasy zwyczajnie zignorują nowe pole podczas obsługi komunikatu.
Dlaczego warto korzystać?
Protocol bufferts przewyższa XML’a w wielu kwestiach, między innymi:
- jest prostszy
- komunikaty zajmują znacząco mniej miejsca
- jest wielokrotnie szybszy
- generuje klasy do obsługi danych, które zazwyczaj są wygodniejsze w obsłudze niż korzystanie z XML’a
A może jednak XML lub JSON?
- przekazywane dane są w formacie czytelnym dla człowieka, można również je w bardzo prosty sposób modyfikować (w przeciwieństwie do Protobuf, który realizuje binarną serializację)
- dane z serwisu można bezpośrednio wyświetlić w przeglądarce
- dużo lepsze wsparcie do obsługi danych w JavaScript
- brak narzuconej struktury przesyłanych danych
- nie ma potrzeby instalacji nowej biblioteki oraz dodatkowego kompilowania klas do jej obsługi
- XML jest formatem samo opisującym się, to znaczy, że nie ma potrzeby korzystania z definicji komunikatu, żeby go zrozumieć
Wybrane funkcjonalności
Generowanie klas Javy
Generowanie gotowych klas Javy polega na uruchomieniu kompilatora Protobuf na plikach wejściowych .proto podając ścieżkę do źródeł oraz katalog na wygenerowany kod. Poniżej przykładowe wywołanie:
bin/protoc -I=./sources --java_out=./dist ./sources/UserProtos.proto
Zapis strumienia do pliku
UserProtos.User user = DataUtils.createUserProtobuf(); try (FileOutputStream fos = new FileOutputStream("User.proto.ser")) { user.writeTo(fos); } catch (IOException ex) { throw new RuntimeException(ex); }
Odczyt komunikatu z pliku
try (FileInputStream fos = new FileInputStream("User.proto.ser")) { UserProtos.User userFromFile = UserProtos.User.parseFrom(fos); } catch (IOException ex) { throw new RuntimeException(ex); }
Instalacja
Bibliotekę możemy skompilować sami ze źródeł lub skorzystać z już wcześniej skompilowanych wersji na wybrany system. Wszystkie wersje wraz ze źródłami dostępne są na github.
Ja posłużę się prekompilowaną wersją protoc-3.1.0-linux-x86_64.zip. Gotowy kompilator jest dostępny pod ścieżką bin/protoc .
Do projektu, poza skompilowanymi już klasami komunikatów Protocol Buffers, trzeba dodać też samą bibliotekę. Najnowsza jej wersja dostępna jest na maven.
<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.1.0</version> </dependency>
Test porównawczy
Protocol Buffers vs Serializacja Java vs JAXB
W ramach testu uruchomiłem 1000 razy serializację użytkownika z przykładu wraz z zapisem do pliku oraz odczyt komunikatu z pliku i jego deserializację.
Test był powtarzany kilkukrotnie i wyniki za każdym razem nieznacznie się różniły, co jest zrozumiałe, ponieważ są to stosunkowo nieduże komunikaty. Dlatego nie należy porównywać wyników co do milisekund, a raczej skupić się porównaniu rządu wielkości.
Wszystkie przykłady oraz kod testów dostępny na github pod adresem: https://github.com/StormITpl/JavaExamples/tree/master/protobuf.
- Protobuf – domyślna implementacja Protobuf
- JAXB – domyślna implementacja JAXB
- JAXB + formatowanie – JAXB z włączonym formatowaniem wyjścia
- JAXB + contex – JAXB z raz utworzonym kontekstem wykorzystanym przy wszystkich operacjach
- Serializacja Java – domyślna serializacja Java
- Serializacja Java + klasy Protobuf – serializacja Java uruchomiona na klasach wygenerowanych przez Protobuf
Protobuf | JAXB | JAXB + formatowanie | JAXB + context | Serializacja Java | Serializacja Java + klasy Protobuf | |
---|---|---|---|---|---|---|
zapis | 85ms | 1936ms | 2047ms | 113ms | 119ms | 98ms |
odczyt | 17ms | 2839ms | 2419ms | 87ms | 99ms | 161ms |
rozmiar komunikatu | 32 bytes | 211 bytes | 267 bytes | 211 bytes | 487 bytes | 224 bytes |
Wnioski
Przeprowadzony test potwierdza to, czym chwalą się autorzy biblioteki. Protobuf rzeczywiście okazał się najszybszy, jeżeli chodzi o serializację i deserializację, wygenerował również najmniejszy komunikat. Różnice są szczególnie widoczne, jeżeli zestawimy to z domyślnym serializowaniem do XML’a przez JAXB. W teście wykorzystano stosunkowo nieduży obiekt, przy większej ilości danych różnice byłyby jeszcze bardziej widoczne.
Czy, w związku z powyższym, Protobuf w przyszłości w pełni zastąpi XML i inne podobne formaty?
Zdecydowanie nie. Mimo swoich wielu zalet i rozbudowanej funkcjonalności nie jest to rozwiązanie idealne dla każdej sytuacji, głównie ze względu na wykorzystanie binarnego zapisu komunikatu.
Biblioteka natomiast wydaje się bardzo dobra dla niezależnej domeny integracyjnej w projektach o wysokich wymaganiach wydajnościowych. W mniejszych projektach, bez takich wymagań, koszt utrzymywania dodatkowej technologii prawdopodobnie przewyższyłby korzyści.
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!
4 Comments
Ciekawy wpis, porównanie wydajności rzeczywiście obrazuje zysk przy użyciu protobuf. Dzięki!
Dzięki Dawid. Protobuf potrafi rzeczywiście przyspieszyć aplikację. Pamiętaj jednak, że nie ma uniwersalnych narzędzi i czasem lepiej wolniej, a prościej.
Jakieś znane zwykłemu zjadaczowi chleba zastosowania poza wewnętrznymi systemami Google? 😉 Bo wydaje mi się, że mimo, że praktycznie „na to oko” widać i każda klasa „zserializowany” w ten sposób, będzie lżejsza, ale kompilacja i dekompilacja to w tej grze potężny narzut czasowy i wielu przypadkach może być tak, że zyskujemy niewielki procent z całego procesu powiedzmy 7-10% czasu. Czy nie jest tak, że nie chodzi o szybszy transfer a raczej wykorzystanie wolniejszych, ale stabilniejszych protokołów transportu?
Sebastian wszystko zależy od tego co chcesz osiągnąć.
Jeżeli wydajność nie jest dla Ciebie kluczowa – to często zwyczajna wygoda w korzystaniu jest najważniejsza.
W takim wypadku rzeczywiście nie warto zawracać sobie głowy tym protokołem, ani innym podobnymi.