UWAGA: witryna została wycofana. Po 31 stycznia 2023 roku witryna zostanie wyłączona, a ruch będzie kierowany do nowej witryny na https://protobuf.dev. Do tego czasu aktualizacje będą dotyczyć tylko protobuf.dev.

Przewodnik po językach

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Z tego przewodnika dowiesz się, jak używać języka bufora protokołu do porządkowania danych bufora protokołu, w tym składni pliku .proto oraz jak generować klasy dostępu do danych z plików .proto. Obejmuje on wersję proto2 języka bufora protokołu. Informacje na temat składni proto3 znajdziesz w Przewodniku po językach proto3.

To jest przewodnik – przykładowy kod, w którym użyto wielu funkcji opisanych w tym dokumencie, znajdziesz w samouczku dla wybranego języka.

Definiowanie typu wiadomości

Przyjrzyjmy się bardzo prostemu przykładowi. Załóżmy, że chcesz zdefiniować format wiadomości z żądaniem wyszukiwania, w którym każde żądanie wyszukiwania zawiera ciąg zapytania, konkretną stronę wyników, które Cię interesują, oraz liczbę wyników na stronie. Oto plik .proto, którego używasz do definiowania typu wiadomości.


message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

Definicja wiadomości SearchRequest określa 3 pola (pary nazw/wartości), po jednym na każdy element danych, który chcesz uwzględnić w wiadomości tego typu. Każde pole ma nazwę i typ.

Określanie typów pól

W powyższym przykładzie wszystkie pola to typy skalarne: 2 liczby całkowite (page_number i result_per_page) oraz ciąg (query). Możesz jednak określić typy złożonych pól, w tym wyliczenia i inne typy wiadomości.

Przypisywanie numerów pól

Jak widać, każde pole w definicji wiadomości ma unikalny numer. Te numery służą do identyfikowania pól w formacie binarnym wiadomości i nie powinny być zmieniane po użyciu typu wiadomości. Kodowanie pól z zakresu od 1 do 15 obejmuje 1 bajt, w tym numer pola i typ pola (więcej informacji na ten temat znajdziesz w artykule Kodowanie buforów protokołów). Numery pól w zakresie od 16 do 2047 roku zajmują 2 bajty. W przypadku bardzo często występujących elementów wiadomości należy zarezerwować numer od 1 do 15. Pamiętaj, aby zrobić miejsce na częste elementy, które mogą zostać dodane w przyszłości.

Najmniejszy numer pola, który można określić, to 1,a największy to 229–1,czyli 536 870 911. Nie możesz też używać numerów z zakresu od 19 000 do 19999 (od FieldDescriptor::kFirstReservedNumber do FieldDescriptor::kLastReservedNumber), ponieważ są one zarezerwowane na potrzeby buforów protokołów – w przypadku korzystania z jednego z tych zarezerwowanych numerów w parametrze .proto kompilator będzie buforować protokół. Nie można też używać żadnych wcześniej zarezerwowanych numerów.

Określanie reguł pól

Określasz, że pola wiadomości są jednym z tych typów:

  • required: poprawnie sformułowana wiadomość musi zawierać dokładnie jedno z tych pól.
  • optional: poprawnie sformułowana wiadomość może mieć zero lub jedno pole (ale nie więcej niż jedno).
  • repeated: to pole może być powtórzone dowolną liczbę razy (w tym zero) w dobrej formie. Kolejność powtarzanych wartości zostanie zachowana.

Ze względów historycznych pola repeated skalarnych typów liczb (na przykład int32, int64, enum) nie są tak kodowane, jak mogłyby być. Nowy kod powinien korzystać ze specjalnej opcji [packed = true], która usprawnia kodowanie. Przykład:

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

Więcej informacji o kodowaniu packed znajdziesz w artykule Kodowanie buforów protokołu.

Wymagane na zawsze. Zachowaj ostrożność podczas oznaczania pól jako required. Jeśli w którymś momencie przestaniesz pisać lub wyślesz wymagane pole, może się pojawić problem, jeśli zmienisz to pole na opcjonalne. Stary czytelnik uzna, że wiadomości bez tego pola jest niepełne, i może go odrzucić lub odrzucić. Zamiast tego rozważ utworzenie niestandardowych procedur weryfikacji buforów dla aplikacji.

Drugi problem z polami wymaganymi pojawi się, gdy ktoś doda wartość do wyliczenia. W tym przypadku nierozpoznana wartość wyliczenia jest traktowana tak, jakby jej nie było, co powoduje, że wymagana weryfikacja wartości się nie powiedzie.

Dodawanie kolejnych typów wiadomości

W jednym pliku .proto można zdefiniować wiele typów wiadomości. Jest to przydatne, jeśli definiujesz wiele powiązanych wiadomości. Jeśli na przykład chcesz zdefiniować format odpowiedzi odpowiadający Twojemu typowi wiadomości w SearchResponse, możesz go dodać do tego samego .proto:


message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

message SearchResponse {
 ...
}

Łączenie wiadomości prowadzi do wymiotu. Mimo że w jednym pliku .proto można zdefiniować wiele typów wiadomości (np. wiadomość, wyliczenie i usługa), może to też prowadzić do zwiększenia liczby zależności. Zalecamy dodanie jak najmniejszej liczby typów wiadomości w pliku .proto.

Dodawanie komentarzy

Aby dodać komentarze do plików .proto, użyj składni C/C++ // i /* ... */.


/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;  // Which page number do we want?
  optional int32 result_per_page = 3;  // Number of results to return per page.
}

Zarezerwowane pola

Jeśli zaktualizujesz typ wiadomości, usuwając całkowicie pole lub dodając komentarz, późniejsi użytkownicy będą mogli ponownie użyć tego pola podczas wprowadzania zmian w tym typie. Może to spowodować poważne problemy, jeśli później wczytają starsze wersje tego samego interfejsu .proto, w tym uszkodzone dane lub błędy prywatności. Jeśli nie chcesz, aby tak się stało, sprawdź, czy numery pól (lub nazwy), które również mogą powodować problemy z serializacją JSON, to reserved. Jeśli w przyszłości któryś z użytkowników spróbuje użyć tych identyfikatorów pól, kompilator bufora otrzyma powiadomienie.

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

Zarezerwowane zakresy numerów pól są uwzględniane (9 to 11 to to samo co 9, 10, 11). Pamiętaj, że nie można łączyć nazw pól i numerów pól w tej samej instrukcji reserved.

Jakie treści generujesz na urządzeniu .proto?

Gdy uruchomisz kompilator bufora protokołu w .proto, kompilator wygeneruje kod w wybranym języku, który będzie służyć do obsługi typów wiadomości opisanych w pliku, takich jak pobieranie i ustawianie wartości pól, serializowanie wiadomości do strumienia wyjściowego oraz analizowanie wiadomości ze strumienia wejściowego.

  • W przypadku C++ kompilator generuje plik .h i .cc z każdego .proto, używając klasy dla każdego typu wiadomości opisanego w pliku.
  • W przypadku Java kompilator generuje plik .java z klasą dla każdego typu wiadomości oraz specjalne klasy Builder do tworzenia instancji wiadomości.
  • Python to trochę inny proces – kompilator Python generuje moduł ze statycznym deskryptorem każdego typu wiadomości w .proto, który jest następnie używany z metaclass do tworzenia niezbędnej klasy dostępu do danych w Pythonie podczas działania.
  • W przypadku Go kompilator generuje plik .pb.go z typem dla każdego typu wiadomości w tym pliku.

Więcej informacji o korzystaniu z interfejsów API w poszczególnych językach znajdziesz w samouczku dla wybranego języka. Więcej informacji o interfejsie API znajdziesz w odpowiednim przewodniku po interfejsie API.

Typy wartości skalarnych

Pole komunikatu skalarnego może mieć jeden z tych typów – tabela pokazuje typ określony w pliku .proto oraz odpowiedni typ w automatycznie wygenerowanej klasie:

Typ .proto Uwagi Typ C++ Typ Java Typ Pythona[2] Typ Go
liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa *liczba zmiennoprzecinkowa 64
liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa *liczba zmiennoprzecinkowa32
Int32 Używa kodowania zmiennej długości. Efektywne kodowanie wartości ujemnych – jeśli pole ma prawdopodobnie wartości ujemne, zamiast tego użyj sint32. Int32 int, int, *int32
int64 Używa kodowania zmiennej długości. Efektywne kodowanie wartości ujemnych – jeśli pole ma prawdopodobnie wartości ujemne, zamiast tego użyj sint64. int64 długi int/long[3] *int64
Uint32 Używa kodowania zmiennej długości. Uint32 int[1] int/long[3] *Uint32
Uint64 Używa kodowania zmiennej długości. Uint64 długi[1] int/long[3] *Uint64
Sint32 Używa kodowania zmiennej długości. Wartość liczbowa ze znakiem. Te kodowanie bardziej efektywnie kodują wartości ujemne niż zwykłe int32. Int32 int, int, *int32
Sint64 Używa kodowania zmiennej długości. Wartość liczbowa ze znakiem. Te kodowanie bardziej efektywnie kodują wartości ujemne niż zwykłe int64. int64 długi int/long[3] *int64
stały3 Zawsze 4 bajty. Bardziej wydajny niż uint32, jeśli wartości są często większe niż 228. Uint32 int[1] int/long[3] *Uint32
stały64 Zawsze 8 bajtów Bardziej wydajny niż uint64, jeśli wartości są często większe niż 256. Uint64 długi[1] int/long[3] *Uint64
spered32 Zawsze 4 bajty. Int32 int, int, *int32
spered64 Zawsze 8 bajtów int64 długi int/long[3] *int64
wartość logiczna wartość logiczna wartość logiczna wartość logiczna *wartość logiczna
tekst Ciąg znaków musi zawsze zawierać tekst zakodowany w formacie UTF-8. tekst Ciąg znaków unicode (Python 2) lub str (Python 3) *ciąg znaków
B Może zawierać dowolną sekwencję bajtów. tekst Ciąg bajtów B []bajt

Więcej informacji o kodowaniu tych typów znajdziesz podczas serializowania wiadomości w sekcji Kodowanie buforów protokołów.

[1] W Javie niepodpisane 32- i 64-bitowe liczby całkowite są reprezentowane przez ich odpowiedniki, a górny fragment jest po prostu przechowywany w bitzie znaku.

[2] We wszystkich przypadkach ustawienie wartości pola pozwala sprawdzić jego typ.

[3] 64-bitowe lub niepodpisane 32-bitowe liczby całkowite są zawsze przedstawiane jako długie po zdekodowaniu, ale w przypadku ustawienia pola mogą zawierać int. We wszystkich przypadkach określona wartość musi pasować do typu reprezentowanego. (zob. [2]).

Pola opcjonalne i wartości domyślne

Jak już wspomnieliśmy, elementy w opisie wiadomości mogą być oznaczone etykietą optional. Prawidłowo sformatowana wiadomość może zawierać element opcjonalny. Gdy wiadomość zostanie przeanalizowana, ale nie będzie zawierać elementu opcjonalnego, dostęp do odpowiedniego pola w przeanalizowanym obiekcie zwróci wartość domyślną tego pola. Wartość domyślna może być określona w opisie wiadomości. Załóżmy na przykład, że chcesz podać domyślną wartość 10 dla wartości result_per_page SearchRequest.

optional int32 result_per_page = 3 [default = 10];

Jeśli element opcjonalny nie jest określony dla elementu opcjonalnego, zamiast niego zostanie użyta wartość domyślna konkretnego typu: w przypadku ciągów znaków domyślna wartość to pusty ciąg znaków. W przypadku bajtów domyślna wartość to pusty ciąg znaków. W przypadku wartości logicznych wartością domyślną jest false (fałsz). W przypadku typów liczbowych wartością domyślną jest 0. W przypadku wyliczenia wartość domyślna jest pierwszą wartością podaną w definicji typu wyliczenia. Oznacza to, że należy zachować ostrożność podczas dodawania wartości na początku listy wartości wyliczenia. Wskazówki na temat bezpiecznej zmiany definicji znajdziesz w sekcji Aktualizowanie typu wiadomości.

Wyliczenia

Podczas definiowania typu wiadomości możesz chcieć, aby jedno z jego pól miało tylko jedną ze wstępnie zdefiniowanych list. Załóżmy, że chcesz dodać pole corpus dla każdego elementu SearchRequest, gdzie korpusem może być UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS lub VIDEO. Możesz to zrobić, dodając do definicji wiadomości enum. Pole z typem enum może mieć tylko jedną z określonych wartości stałych (jeśli spróbujesz podać inną wartość, parser potraktuje go jak nieznane pole). W tym przykładzie dodaliśmy tag enum o nazwie Corpus ze wszystkimi możliwymi wartościami i pole Corpus:

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
}

Aliasy możesz definiować, przypisując tę samą wartość do różnych stałych wartości enum. Aby to zrobić, musisz ustawić opcję allow_alias na true. W przeciwnym razie po wykryciu aliasów kompilator bufora protokołu wygeneruje komunikat o błędzie. Wszystkie wartości aliasów są prawidłowe podczas deserializacji, ale pierwsza wartość jest zawsze używana do serializacji.

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  ENAA_FINISHED = 2;
}

Stałe licznika muszą mieścić się w zakresie 32-bitowej liczby całkowitej. Wartości enum używają przejścia kodowania w przewodzie, dlatego wartości ujemne są nieskuteczne i nie są zalecane. Wiadomości enum możesz definiować w definicji wiadomości lub poza nią – tych atrybutów enum możesz używać w dowolnej definicji wiadomości w pliku .proto. Możesz też użyć typu enum zadeklarowanego w jednej wiadomości jako typu pola w innej wiadomości, używając składni _MessageType_._EnumType_.

Po uruchomieniu kompilatora bufora protokołu na urządzeniu .proto, które korzysta z tagu enum, będzie on miał odpowiednią klasę enum dla języka Java lub C++ albo specjalną klasę EnumDescriptor dla języka Python, która służy do tworzenia zestawów wartości stałych wraz z symbolami liczby całkowitej.

Więcej informacji o tym, jak pracować z wiadomościami enum w aplikacjach, znajdziesz w przewodniku po kodzie wygenerowanym dla wybranego języka.

Zarezerwowane wartości

Jeśli zaktualizujesz typ wyliczenia, całkowicie usuwając go lub skomentując, przyszli użytkownicy będą mogli używać tej wartości liczbowej podczas wprowadzania zmian w typie. Może to spowodować poważne problemy, jeśli później wczytają starsze wersje tego samego interfejsu .proto, w tym uszkodzone dane lub błędy prywatności. Jeśli nie chcesz, aby tak się stało, sprawdź, czy wartości liczbowe (lub nazwy), które mogą powodować problemy także w przypadku serializacji JSON, to reserved. Jeśli w przyszłości któryś z użytkowników spróbuje użyć tych identyfikatorów, kompilator będzie kompilować bufor protokołu. Za pomocą słowa kluczowego max możesz określić, że zarezerwowany zakres liczbowy osiągnie maksymalną możliwą wartość.


enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

Pamiętaj, że nie można łączyć nazw pól i wartości liczbowych w tej samej instrukcji reserved.

Korzystanie z innych typów wiadomości

Jako typów pól możesz użyć innych typów wiadomości. Załóżmy na przykład, że chcesz umieścić wiadomości Result w każdej wiadomości SearchResponse. W tym celu możesz zdefiniować typ wiadomości Result w tym samym tagu .proto, a następnie określić pole typu Result w polu SearchResponse:


message SearchResponse {
  repeated Result result = 1;
}

message Result {
  required string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

Importowanie definicji

W powyższym przykładzie typ wiadomości Result jest zdefiniowany w tym samym pliku co SearchResponse – co w sytuacji, gdy typ wiadomości, którego chcesz użyć jako typu pola, jest już zdefiniowany w innym pliku .proto?

Aby użyć definicji z innych plików .proto, zaimportuj je. Aby zaimportować definicje innego elementu .proto, dodaj na początku pliku instrukcję importu:

import "myproject/other_protos.proto";

Domyślnie możesz używać definicji tylko z bezpośrednio zaimportowanych plików .proto. Czasem jednak trzeba przenieść plik .proto do nowej lokalizacji. Zamiast bezpośrednio przenosić plik .proto i aktualizować wszystkie witryny wywołujące w ramach jednej zmiany, możesz umieścić plik zastępczy .proto w starej lokalizacji, aby przekierować wszystkie zaimportowane dane do nowej lokalizacji za pomocą zapisu import public.

Pamiętaj, że funkcja importowania publicznego nie jest dostępna w języku Java.

Zależności import public mogą być zależne od każdego kodu, który importuje proto zawierające instrukcję import public. Przykład:

// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

Kompilator protokołu wyszukuje zaimportowane pliki w zbiorze katalogów określonym w wierszu polecenia kompilatora za pomocą flagi -I/--proto_path. Jeśli nie podano flagi, znajduje się ona w katalogu, w którym wywołano kompilator. Ogólnie flagę --proto_path należy ustawić na poziomie głównym projektu, używając wszystkich pełnych nazw dla wszystkich importów.

Korzystanie z typu wiadomości proto3

Można importować typy wiadomości proto3 i używać ich w wiadomościach proto2 i odwrotnie. W składni proto3 nie można jednak używać wyliczenia proto2.

Typy zagnieżdżone

Możesz zdefiniować i używać typów wiadomości w innych typach wiadomości, tak jak w tym przykładzie: wiadomość Result jest zdefiniowana w wiadomości SearchResponse:

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

Jeśli chcesz używać tego typu wiadomości więcej niż w ramach jego nadrzędnej wiadomości, musisz używać nazwy _Parent_._Type_:

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}

Wiadomości możesz zagnieżdżać, jak chcesz. W poniższym przykładzie dwa zagnieżdżone typy Inner są całkowicie niezależne, ponieważ są zdefiniowane w różnych wiadomościach:

message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      optional int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      optional string name = 1;
      optional bool   flag = 2;
    }
  }
}

Grupy

Pamiętaj, że funkcja grup została wycofana i nie należy jej używać do tworzenia nowych typów wiadomości. Zamiast nich użyj zagnieżdżonych typów wiadomości.

Grupy to kolejny sposób na zagnieżdżanie informacji w definicjach wiadomości. Innym sposobem określenia atrybutu SearchResponse z liczbą Result jest na przykład:

message SearchResponse {
  repeated group Result = 1 {
    required string url = 2;
    optional string title = 3;
    repeated string snippets = 4;
  }
}

Grupa po prostu łączy zagnieżdżony typ wiadomości i pole w jedną deklarację. W kodzie możesz traktować tę wiadomość tak, jakby zawierała pole typu Result o nazwie result (ta druga nazwa jest konwertowana na małe litery, dzięki czemu nie koliduje ona z poprzednią). Dlatego ten przykład jest dokładnie taki sam jak SearchResponse powyżej, z tą różnicą, że wiadomość ma inny format przewodowy.

Aktualizowanie typu wiadomości

Jeśli istniejący typ wiadomości nie spełnia już wszystkich Twoich oczekiwań, czyli na przykład format wiadomości ma zawierać dodatkowe pole, ale nadal chcesz używać kodu utworzonego w starym formacie, nie musisz się martwić. Gdy korzystasz z formatu przewodów binarnych, możesz łatwo aktualizować typy wiadomości bez naruszania istniejącego kodu.

Jeśli używasz formatu przewodu binarnego, sprawdź te reguły:

  • Nie zmieniaj numerów pól, które już masz w polu.
  • Nowe pola powinny mieć wartość optional lub repeated. Oznacza to, że wszystkie wiadomości zserializowane według kodu przy użyciu „starego” formatu wiadomości mogą zostać przeanalizowane przez nowo wygenerowany kod, ponieważ nie brakuje w nich żadnych elementów required. Dla tych elementów należy ustawić rozsądne wartości domyślne, aby nowy kod mógł odpowiednio współdziałać z wiadomościami wygenerowanymi za pomocą starego kodu. Analogicznie wiadomości utworzone przez nowy kod można przeanalizować za pomocą starego kodu: stare pliki binarne po prostu zignorują nowe pole podczas analizy. Nieznane pola nie zostaną jednak odrzucone, a jeśli wiadomość zostanie później zserializowana, te pola zostaną zserializowane razem z wiadomością, więc jeśli wiadomość zostanie przekazana do nowego kodu, nowe pola będą nadal dostępne.
  • Niewymagane pola można usunąć, pod warunkiem że numer pola nie będzie ponownie używany w zaktualizowanym typie wiadomości. Zamiast tego możesz zmienić nazwę pola, dodając przedrostek „OBSOLETE_” lub zarezerwować numer pola, aby przyszłe konta użytkowników .proto nie mogły ponownie użyć tego numeru.
  • Niewymagane pole można przekształcić w rozszerzenie i odwrotnie, o ile typ i numer się nie zmieniają.
  • Wartości int32, uint32, int64, uint64 i bool są zgodne. Oznacza to, że możesz zmienić pole z jednego typu na inny bez naruszania zgodności. Jeśli liczba jest przeanalizowana za pomocą przewodu, który nie pasuje do odpowiedniego typu, uzyskasz taki sam efekt jak przy przesyłaniu numeru do C++ (na przykład liczba 64-bitowa podana w postaci int32 zostanie skrócona do 32 bitów).
  • Typy sint32 i sint64 są ze sobą zgodne, ale nie są zgodne z innymi typami liczb całkowitych.
  • Znaki string i bytes są zgodne, o ile bajty są prawidłowymi kodami UTF-8.
  • Umieszczone wiadomości są zgodne z bytes, jeśli bajty zawierają zakodowaną wersję wiadomości.
  • fixed32 jest zgodny z sfixed32 oraz fixed64 z sfixed64.
  • W przypadku pól string, bytes i wiadomości optional jest zgodny z repeated. Biorąc pod uwagę zserializowane dane pola powtarzanego jako dane wejściowe, klienty, które oczekują, że to pole będzie mieć wartość optional, przyjmie ostatnią wartość wejściową, jeśli jest to pole typu podstawowego, albo scal wszystkie elementy wejściowe, jeśli jest to pole typu wiadomości. Pamiętaj, że nie jest to zazwyczaj bezpieczne w przypadku typów liczbowych, takich jak wybudzenia i wyliczenia. Powtórzone pola typów liczbowych można zserializować w formacie spakowanym, który nie zostanie prawidłowo przeanalizowany, gdy pole optional będzie oczekiwane.
  • Zmiana wartości domyślnej jest ogólnie dozwolona, o ile wartości domyślne nie są przesyłane przewodnie. Oznacza to, że jeśli program otrzyma komunikat, w którym pole nie jest ustawione, zobaczy wartość domyślną, która została zdefiniowana w wersji protokołu wybranego w tym programie. NIE wskazuje on wartości domyślnej, która została zdefiniowana w kodzie nadawcy.
  • Protokół enum jest zgodny z formatami int32, uint32, int64 i uint64 w formie przewodowej (wartości mogą być obcinane, jeśli się nie mieszczą), ale pamiętaj, że kod klienta może być traktowany inaczej w przypadku deskrypcji. Warto zauważyć, że nierozpoznane wartości enum są odrzucane, gdy wiadomość jest preferencyjna, przez co akcesor has.. pola zwraca wartość fałsz, a metoda pobierania zwraca pierwszą wartość podaną w definicji enum lub wartość domyślną, jeśli została określona. W przypadku powtórzonych pól wyliczenia wszystkie nierozpoznane wartości są usuwane z listy. Pole liczby całkowitej zawsze jednak zachowuje wartość. Z tego powodu musisz zachować ostrożność podczas aktualizowania liczby całkowitej do enum, aby nie dopuścić do przekroczenia wartości enum w przewodzie.
  • W bieżących implementacjach Java i C++ po usunięciu nierozpoznanych wartości enum są one przechowywane razem z innymi nieznanymi polami. Może to powodować dziwne zachowanie, jeśli te dane zostaną poddane serializacji, a następnie porównane z klientem, który rozpoznaje te wartości. W przypadku pól opcjonalnych nawet wtedy, gdy nowa wartość została zapisana po destylacji oryginalnej wiadomości, poprzednia wartość jest nadal odczytywana przez klientów, którzy ją rozpoznają. W przypadku pól powtarzanych stare wartości będą wyświetlane po każdej rozpoznanej i nowo dodanej wartości. Oznacza to, że kolejność nie zostanie zachowana.
  • Zmiana pojedynczego pola lub rozszerzenia optional w członek nowego elementu oneof jest zgodny z systemem binarnym, ale w niektórych językach (zwłaszcza w języku Go) interfejs API wygenerowanego kodu zmieni się w niezgodny sposób. Z tego powodu Google nie wprowadza takich zmian w swoich publicznych interfejsach API, co opisano w AIP-180. Z zastrzeżeniem tych samych zasad dotyczących zgodności źródła przenoszenie wielu pól do nowego interfejsu oneof może być bezpieczne, jeśli nie masz pewności, że w danym momencie nie ustawisz kodu więcej niż 1. Przenoszenie pól do istniejącego zasobu oneof nie jest bezpieczne. I podobnie, zmiana jednego pola oneof na pole lub rozszerzenie optional jest bezpieczna.
  • Zmiana pola między właściwością map<K, V> a odpowiadającym mu polem repeated jest zgodna z danymi binarnymi (poniżej znajdziesz opis układu Map oraz inne ograniczenia). Bezpieczeństwo zmiany zależy jednak od aplikacji: podczas opuszczania i powielania wiadomości klienci używający definicji pola repeated otrzymają wynik semantyczny, ale klienci używający definicji pola map mogą zmieniać kolejność wpisów i usuwać je ze zduplikowanymi kluczami.

Rozszerzenia

Rozszerzenia umożliwiają zadeklarowanie, że w przypadku rozszerzeń innych firm dostępny jest zakres pól pól w wiadomości. Rozszerzenie to obiekt zastępczy pola, którego typ nie jest określony w pierwotnym pliku .proto. Dzięki temu inne pliki .proto mogą być dodawane do definicji wiadomości poprzez zdefiniowanie typów niektórych lub wszystkich pól z tymi numerami pól. Spójrzmy na przykład:

message Foo {
  // ...
  extensions 100 to 199;
}

Oznacza to, że zakres numerów pól [100, 199] w polu Foo jest zarezerwowany dla rozszerzeń. Inni użytkownicy mogą teraz dodawać nowe pola do usługi Foo w swoich własnych plikach .proto, które importują .proto, korzystając z numerów pól w określonym zakresie – na przykład:

extend Foo {
  optional int32 bar = 126;
}

Spowoduje to dodanie pola o nazwie bar z polem numer 126 w pierwotnej definicji Foo.

Gdy wiadomości Foo są zakodowane, format przewodu jest taki sam jak w przypadku użytkownika zdefiniowanego przez nowe pole w polu Foo. Jednak sposób uzyskiwania dostępu do pól rozszerzeń w kodzie aplikacji różni się nieco od zwykłego pola – wygenerowany kod dostępu do danych zawiera specjalne metody dostępu do rozszerzeń. Oto przykład ustawienia wartości bar w języku C++:

Foo foo;
foo.SetExtension(bar, 15);

Klasa Foo definiuje też akcesory szablonowe HasExtension(), ClearExtension(), GetExtension(), MutableExtension() i AddExtension(). Wszystkie mają semantykę pasującą do odpowiadających im wygenerowanych akcesorów dla normalnego pola. Więcej informacji o korzystaniu z rozszerzeń znajdziesz w wygenerowanym pliku referencyjnym dla wybranego języka.

Pamiętaj, że rozszerzenia mogą być polami dowolnego typu, w tym z różnymi rodzajami wiadomości, ale nie mogą być mapami ani jednym z nich.

Rozszerzenia zagnieżdżone

Rozszerzenia można zadeklarować w zakresie innego typu:

message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}

W tym przypadku kod C++ umożliwiający dostęp do tego rozszerzenia to:

Foo foo;
foo.SetExtension(Baz::bar, 15);

Innymi słowy jedyny efekt to to, że właściwość bar jest zdefiniowana w zakresie Baz.

Jest to typowe źródło niejasności: zadeklarowanie bloku extend w typie wiadomości nie sugeruje powiązania między typem zewnętrznym a rozszerzonym. W tym przykładzie nie oznacza, że Baz jest dowolną z podkategorii Foo. Oznacza jedynie, że symbol bar jest zadeklarowany w zakresie Baz. Jest to tylko element statyczny.

Typowym wzorcem jest definiowanie rozszerzeń należących do zakresu pola rozszerzenia – na przykład rozszerzenie Foo do typu Baz, gdzie rozszerzenie jest zdefiniowane jako część Baz:

message Baz {
  extend Foo {
    optional Baz foo_ext = 127;
  }
  ...
}

Nie ma jednak wymagania, aby rozszerzenie z typem wiadomości było zdefiniowanym w tym typie. Możesz to też zrobić:

message Baz {
  ...
}

// This can even be in a different file.
extend Foo {
  optional Baz foo_baz_ext = 127;
}

W rzeczywistości ta składnia może być preferowana, by uniknąć nieporozumień. Jak już wspomnieliśmy, zagnieżdżona składnia jest często mylona z klasyfikacją podrzędną przez użytkowników, którzy jeszcze nie znają rozszerzeń.

Wybieranie numerów rozszerzeń

Bardzo ważne jest, aby upewnić się, że 2 użytkowników nie dodaje rozszerzeń do tego samego typu wiadomości przy użyciu tego samego numeru pola – uszkodzone dane mogą zostać przypadkowo zinterpretowane jako nieprawidłowy typ. Aby zapobiec takiej sytuacji, możesz rozważyć zdefiniowanie konwencji numerowania rozszerzeń w projekcie.

Jeśli konwencja numeracji może obejmować rozszerzenia o bardzo dużych numerach pól, za pomocą słowa kluczowego max możesz ustawić zakres rozszerzenia aż do maksymalnego możliwego numeru pola:

message Foo {
  extensions 1000 to max;
}

max to 229 – 1, czyli 536 870 911.

Podobnie jak w przypadku wybierania numerów pól, konwencja numeracji musi także unikać liczb pól 19 000–19999 (FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber), ponieważ są one zarezerwowane na potrzeby buforów protokołów. Możesz zdefiniować zakres rozszerzeń obejmujący ten zakres, ale kompilator protokołów nie pozwala na zdefiniowanie rzeczywistych rozszerzeń z tymi wartościami.

Oneof

Jeśli masz wiadomość z wieloma polami opcjonalnymi, w których możesz ustawić jednocześnie jedno pole, możesz wymusić stosowanie tego ustawienia i zaoszczędzić pamięć dzięki funkcji „oneof”.

Jedno z pól jest jak pola opcjonalne, z wyjątkiem wszystkich pól w pamięci współdzielonej. Jednocześnie można ustawić maksymalnie jedno pole. Ustawienie dowolnego z nich automatycznie usuwa pozostałe dane. W zależności od wybranego języka możesz sprawdzić, która wartość w jednym jest ustawiona (jeśli jest dostępna) przy użyciu specjalnej metody case() lub WhichOneof().

Korzystanie z Oneof

Aby zdefiniować jedno z nich w elemencie .proto, użyj słowa kluczowego oneof, a następnie Twojego imienia i nazwiska, w tym przypadku test_oneof:

message SampleMessage {
  oneof test_oneof {
     string name = 4;
     SubMessage sub_message = 9;
  }
}

Następnie dodaj pola oneof do definicji onof. Możesz dodawać pola dowolnego typu, ale nie możesz używać słów kluczowych required, optional ani repeated. Jeśli chcesz dodać pole powtarzane do jednego z pól, możesz użyć wiadomości zawierającej pole powtarzane.

W wygenerowanym kodzie jedno z pól ma te same metody pobierania i pobierania co zwykłe metody optional. Możesz też skorzystać ze specjalnej metody sprawdzania, która wartość (jeśli jest dostępna) jest ustawiona. Więcej informacji o interfejsie API Oneof w wybranym języku znajdziesz w odpowiednim przewodniku po interfejsie API.

Funkcje Oneof

  • Ustawienie jednego pola automatycznie spowoduje usunięcie wszystkich pozostałych użytkowników. Jeśli ustawisz kilka z nich, tylko ostatnie pole będzie miało wartość.

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    message.mutable_sub_message();   // Will clear name field.
    CHECK(!message.has_name());
    
  • Jeśli parser napotka kilku członków tego samego przewodu, w analizowanej wiadomości zostanie użyty tylko ostatni zarejestrowany element.

  • Rozszerzenia nie są obsługiwane w przypadku tego rozszerzenia.

  • Jedno z tych wartości nie może być wartością repeated.

  • Interfejsy refleksyjne działają w przypadku jednego z pól.

  • Jeśli w polu oneof wyświetlisz wartość domyślną (np. pole „int32 oneof” ma wartość 0), wielkość liter tego pola zostanie ustawiona, a wartość zostanie zserializowana

  • Jeśli używasz C++, upewnij się, że kod nie powoduje awarii pamięci. Ten przykładowy kod spowoduje awarię, bo sub_message został już usunięty przez wywołanie metody set_name().

    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
  • W C++ jeśli Swap() oznaczysz dwie wiadomości jednym z nich, każda wiadomość zakończy się jedną z nich: w przykładzie poniżej msg1 będzie miał sub_message, a msg2 będzie mieć name.

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

Problemy ze zgodnością wsteczną

Przy dodawaniu lub usuwaniu jednego z tych pól należy zachować ostrożność. Jeśli sprawdzanie wartości jednego z nich zwraca wartość None/NOT_SET, może to oznaczać, że nie jest on ustawiony lub jest ustawiony na pole w innej wersji. Nie ma sposobu, aby to rozróżnić, ponieważ nie można stwierdzić, czy nie należą do niego nieznane pole.

Problemy z ponownym użyciem tagów

  • Przenoszenie opcjonalnych pól do lub z jednego z pól: po zserializowaniu i przeanalizowaniu wiadomości możesz utracić niektóre informacje (niektóre pola zostaną usunięte). Możesz jednak bezpiecznie przenieść jedno pole do nowego, a jeśli wybrane zostanie tylko jedno, możesz przenieść wiele pól. Więcej informacji znajdziesz w sekcji Aktualizowanie typu wiadomości.
  • Usuwanie pola oneof i dodanie go z powrotem: może to spowodować usunięcie obecnego pola oneof także po zserializowaniu i przeanalizowaniu wiadomości.
  • Podziel lub scal pole: ten sam problem występuje przy przenoszeniu zwykłych pól optional.

Mapy

Jeśli chcesz utworzyć mapę skojarzoną w ramach definicji danych, przydatne są bufory protokołu:

map<key_type, value_type> map_field = N;

...gdzie key_type może być dowolnym ciągiem integralnym lub ciągiem (w efekcie dowolny typ skalarski, z wyjątkiem typów zmiennoprzecinkowych i bytes). Wyliczenie nie jest prawidłowym key_type. value_type może być dowolnym typem poza mapą.

Jeśli chcesz np. utworzyć mapę projektów, w których każda wiadomość Project jest powiązana z kluczem ciągu znaków, możesz ją zdefiniować w ten sposób:

map<string, Project> projects = 3;

Wygenerowany interfejs API mapy jest obecnie dostępny we wszystkich językach obsługiwanych przez proto2. Więcej informacji o interfejsie API Map dla wybranego języka znajdziesz w odpowiednim przewodniku po interfejsie API.

Funkcje Map

  • Mapy nie obsługują rozszerzeń.
  • Mapy nie mogą działać w trybie repeated, optional ani required.
  • Kolejność formatów i wartości iteracji map jest niezdefiniowana, więc nie możesz polegać na tym, że elementy mapy znajdują się w określonej kolejności.
  • Podczas generowania formatu tekstu dla elementu .proto mapy są sortowane według klucza. Klucze numeryczne są sortowane numerycznie.
  • Podczas analizowania przewodu lub scalania w przypadku zduplikowanych kluczy mapy używany jest ostatni widoczny klucz. Analiza mapy z formatu tekstowego może zakończyć się niepowodzeniem w przypadku zduplikowanych kluczy.

Zgodność wsteczna

Składnia mapy jest taka sama jak przedstawiona na przewodzie, więc implementacje buforów protokołów, które nie obsługują map, mogą nadal obsługiwać Twoje dane:

message MapFieldEntry {
  optional key_type key = 1;
  optional value_type value = 2;
}

repeated MapFieldEntry map_field = N;

Każda implementacja bufora protokołu, która obsługuje mapy, musi generować i akceptować dane, które można zaakceptować zgodnie z powyższą definicją.

Pakiety

Aby zapobiec konfliktom nazw między typami wiadomości protokołów, do pliku .proto możesz dodać opcjonalny specyfikator package.

package foo.bar;
message Open { ... }

Następnie możesz użyć specyfikacji pakietu do zdefiniowania pól typu wiadomości:

message Foo {
  ...
  required foo.bar.Open open = 1;
  ...
}

Wpływ specyfikacji pakietu na wygenerowany kod zależy od wybranego języka:

  • W C++ wygenerowane zajęcia są opakowane w przestrzeni nazw C++. Na przykład w przestrzeni nazw foo::bar będzie Open.
  • W Java pakiet jest używany jako pakiet w języku Java, chyba że wyraźnie określisz plik option java_package w pliku .proto.
  • W Pythonie dyrektywa package jest ignorowana, ponieważ moduły w Pythonie są uporządkowane według lokalizacji w systemie plików.
  • W zasadzie Go dyrektywa package jest ignorowana, a wygenerowany plik .pb.go znajduje się w pakiecie o takiej samej nazwie jak odpowiednia reguła go_proto_library.

Pamiętaj, że nawet jeśli dyrektywa package nie wpływa bezpośrednio na wygenerowany kod, na przykład w Pythonie, zdecydowanie zalecamy określenie pakietu pliku .proto. W przeciwnym razie może to prowadzić do konfliktów w deskryptorach i uniemożliwić przenoszenie proto do innych języków.

Rozwiązania w zakresie pakietów i nazw

Rozpoznawanie nazw w języku bufora protokołu działa podobnie do języka C++: najpierw przeszukiwany jest zakres najniższy, następnie następny w kolejności itd., przy czym każdy pakiet jest uznawany za „wewnętrzny” w stosunku do pakietu nadrzędnego. Początkowy znak „.” (np. .foo.bar.Baz) oznacza, że zaczynasz od zewnętrznego zakresu.

Kompilator bufora protokołu rozpoznaje nazwy wszystkich typów, analizując zaimportowane pliki .proto. Generator kodu w przypadku każdego języka wie, jak się odwoływać do każdego typu w tym języku, nawet jeśli ma on różne reguły zakresu.

Usługi definiowania

Jeśli chcesz używać typów wiadomości z systemem RPC (zdalne wywołanie procedury), możesz zdefiniować interfejs usługi RPC w pliku .proto, a kompilator bufora protokołu wygeneruje kod interfejsu usługi i namioty w wybranym języku. Jeśli na przykład chcesz zdefiniować usługę RPC za pomocą metody, która przyjmuje wartość SearchRequest i zwraca wartość SearchResponse, możesz ją zdefiniować w pliku .proto w ten sposób:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

Domyślnie kompilator protokołów wygeneruje abstrakcyjny interfejs o nazwie SearchService i powiązaną z nim implementację. Strój przekazuje wszystkie wywołania do RpcChannel, co z kolei jest abstrakcyjnym interfejsem, który należy zdefiniować jako własny system RPC. Możesz na przykład wdrożyć RpcChannel, która szereguje wiadomość i wysyła ją na serwer przez HTTP. Inaczej mówiąc, wygenerowane namiastki pozwalają utworzyć bezpieczne dla typu interfejs do wykonywania wywołań RPC opartych na buforze bez konieczności stosowania konkretnej implementacji RPC. Kod w C++ może wyglądać tak:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // The protocol compiler generates the SearchService class based on the
  // definition given above.
  service = new SearchService::Stub(channel);

  // Set up the request.
  request.set_query("protocol buffers");

  // Execute the RPC.
  service->Search(controller, &request, &response,
                  protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}

Wszystkie klasy usługi implementują też interfejs Service, który umożliwia wywoływanie konkretnych metod bez poznania nazwy metody ani typów danych wejściowych i typów danych wyjściowych w czasie kompilacji. Po stronie serwera można to wykorzystać do wdrożenia serwera RPC, przy użyciu którego można zarejestrować usługi.

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // You provide class MyRpcServer.  It does not have to implement any
  // particular interface; this is just an example.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}

Jeśli nie chcesz podłączyć własnego systemu RPC, możesz teraz użyć narzędzia gRPC: opracowanego przez Google systemu RPC wykorzystującego język i platformę, które nie wymaga integracji. Doskonale sprawdza się zwłaszcza w przypadku buforów protokołów i umożliwia generowanie odpowiedniego kodu RPC bezpośrednio z plików .proto za pomocą specjalnej wtyczki do kompilatora buforów. Ponieważ jednak mogą wystąpić problemy ze zgodnością między klientami a serwerami wygenerowanymi za pomocą proto2 i proto3, zalecamy definiowanie usług gRPC za pomocą protokołu proto3. Więcej informacji o składni proto3 znajdziesz w Przewodniku po języku Proto3. Jeśli chcesz używać protokołu proto2 z gRPC, musisz użyć kompilatora i bibliotek protokołów w wersji 3.0.0 lub nowszej.

Oprócz gRPC istnieje też szereg bieżących projektów innych firm, które opracowują implementacje RPC dla buforów protokołów. Listę linków do znanych nam projektów znajdziesz na stronie wiki o dodatkach innych firm.

Opcje

Do poszczególnych deklaracji w pliku .proto można dodawać adnotacje z różnymi opcjami. Opcje nie zmieniają ogólnego znaczenia deklaracji, ale mogą wpływać na sposób jej wykorzystania w konkretnym kontekście. Pełną listę dostępnych opcji znajdziesz w /google/protobuf/descriptor.proto.

Niektóre z tych opcji to opcje na poziomie pliku, co oznacza, że powinny być one zapisywane w zakresie najwyższego poziomu, a nie w żadnej wiadomości, wyliczeniu czy definicji usługi. Niektóre z nich są opcjami na poziomie wiadomości, co oznacza, że powinny być zapisane w definicjach wiadomości. Niektóre z nich są opcjami na poziomie pola. Oznacza to, że powinny być zapisane w definicjach pól. Opcje mogą być również zapisywane na podstawie typów wyliczenia, wartości wyliczenia, jednego z pól, typów usług i metod usługi; w przypadku żadnego z nich obecnie nie ma żadnych użytecznych opcji.

Oto kilka najpopularniejszych z nich:

  • java_package (opcja pliku): pakiet, który ma być używany dla wygenerowanych klas Java. Jeśli w pliku .proto nie zostanie podana żadna opcja java_package, domyślnie używany będzie pakiet proto (określony słowem kluczowym „package” w pliku .proto). Pakiety proto zwykle nie generują dobrych pakietów Java, ponieważ nie powinny zaczynać się odwrotnymi nazwami domen. Jeśli nie generujesz kodu w języku Java, ta opcja nie ma żadnego efektu.

    option java_package = "com.example.foo";
    
  • java_outer_classname (opcja pliku): nazwa klasy (a tym samym nazwa) klasy JavaScript kodu, który chcesz wygenerować. Jeśli w pliku .proto nie określono jednoznacznego elementu java_outer_classname, nazwa klasy zostanie utworzona poprzez przekonwertowanie nazwy pliku .proto na wielkość liter. W takim przypadku opcja java_multiple_files zostanie wyłączona, a wszystkie inne klasy/wyliczenia itp. wygenerowane dla pliku .proto zostaną wygenerowane w tej zewnętrznej klasie kodu Java jako zagnieżdżone klasy/enum/itp. Jeśli nie ma żadnego kodu Java, ta opcja nie generuje kodu Java.

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files (opcja pliku): jeśli ten plik ma wartość false, zostanie wygenerowany tylko 1 plik .java, a wszystkie klasy/enum/anulowanie języka Java itd. wygenerowane dla najwyższego poziomu wiadomości, usług i wyliczeń zostaną zagnieżdżone w klasie zewnętrznej (patrz java_outer_classname). Jeśli prawda, oddzielne pliki .java zostaną wygenerowane dla poszczególnych klas lub klasyków dla tego poziomu, Jeśli nie generujesz kodu w języku Java, ta opcja nie ma żadnych skutków.

    option java_multiple_files = true;
    
  • optimize_for (opcja pliku): można ustawić wartość SPEED, CODE_SIZE lub LITE_RUNTIME. Ma to wpływ na generatory kodów C++ i Javy (a także zewnętrzne generatory):

    • SPEED (domyślnie): kompilator bufora protokołu wygeneruje kod do serializacji, analizowania i wykonywania innych typowych operacji na typach wiadomości. Ten kod jest bardzo zoptymalizowany.
    • CODE_SIZE: kompilator bufora protokołu generuje minimalną liczbę zajęć i będzie korzystać z udostępnianego kodu opartego na odbiciu do wdrożenia serializacji, analizy i innych operacji. Wygenerowany kod będzie więc znacznie mniejszy niż w SPEED, ale operacje będą wolniejsze. Klasy będą nadal implementować dokładnie ten sam publiczny interfejs API, co w trybie SPEED. Ten tryb jest szczególnie przydatny w aplikacjach, które zawierają bardzo dużo plików .proto i nie wymagają, aby wszystkie były szybkie.
    • LITE_RUNTIME: kompilator bufora protokołu generuje klasy, które zależą tylko od „biblioteki wykonawczej Lite” (libprotobuf-lite zamiast libprotobuf). Środowisko wykonawcze Lite jest znacznie mniejsze niż cała biblioteka (wokół mniejszej wielkości), ale pomija niektóre funkcje, takie jak deskryptory i odbicia. Jest to szczególnie przydatne w aplikacjach na ograniczonych platformach, takich jak telefony komórkowe. Kompilator wygeneruje szybkie implementacje wszystkich metod, tak jak w trybie SPEED. Wygenerowane klasy będą implementować interfejs MessageLite tylko w każdym języku, co zapewnia tylko podzbiór metod pełnego interfejsu Message.
    option optimize_for = CODE_SIZE;
    
  • cc_generic_services, java_generic_services, py_generic_services (opcje plików): czy kompilator bufora protokołu ma generować abstrakcyjny kod usługi na podstawie odpowiednio definicji usług w C++, Java czy Python. Z powodu starszych wersji te opcje domyślne to true. Jednak od wersji 2.3.0 (styczeń 2010) zalecane jest, aby implementacje RPC udostępniały wtyczki generatora kodu do generowania kodu dopasowanego do konkretnego systemu, zamiast korzystać z usług „abstrakcyjnych”.

    // This file relies on plugins to generate service code.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
  • cc_enable_arenas (opcja pliku): włącza alokację obszaru dla kodu wygenerowanego przez C++.

  • message_set_wire_format (opcja wiadomości): jeśli ta opcja jest ustawiona, true ma inny format binarny, który jest zgodny ze starym formatem o nazwie MessageSet. Użytkownicy spoza Google prawdopodobnie nigdy nie będą musieli korzystać z tej opcji. Komunikat musi być zadeklarowany w następujący sposób:

    message Foo {
      option message_set_wire_format = true;
      extensions 4 to max;
    }
    
  • packed (opcja pola): jeśli pole true ma typ powtarzany w typie podstawowym, stosowane jest bardziej kompaktowe kodowanie. Korzystanie z tej opcji nie ma żadnych negatywnych zalet. Pamiętaj jednak, że przed wersją 2.3.0 parsery, które otrzymały spakowane dane, gdy były nieoczekiwane, zostały zignorowane. W związku z tym nie można zmienić obecnego pola na format spakowany bez naruszania zgodności przewodów. W wersjach 2.3.0 i nowszych ta zmiana jest bezpieczna, ponieważ parsery w polach do pakowania zawsze akceptują oba formaty, ale zachowaj ostrożność w przypadku starych programów, które korzystają ze starych wersji protobuf.

    repeated int32 samples = 4 [packed = true];
    
  • deprecated (opcja pola): jeśli ma wartość true, wskazuje, że pole jest wycofane i nie powinno być używane przez nowy kod. W większości języków nie ma to żadnego efektu. W Javie staje się ona adnotacją @Deprecated. W przypadku C++ clang-tidy będzie generować ostrzeżenia za każdym razem, gdy używane będą wycofane pola. W przyszłości inne generatory kodu powiązane z określonym językiem mogą generować adnotacje o wycofaniu elementów metody dostępu, co z kolei spowoduje wygenerowanie ostrzeżenia podczas kompilowania kodu próbującego wykorzystać to pole. Jeśli pole nie jest używane przez nikogo i chcesz uniemożliwić korzystanie z niego nowym użytkownikom, zastąp deklarację pola zastrzeżoną instrukcją.

    optional int32 old_field = 6 [deprecated=true];
    

Opcje niestandardowe

Bufory protokołu pozwalają nawet określać i używać własnych opcji. Pamiętaj, że jest to funkcja zaawansowana, której większość osób nie potrzebuje. Ponieważ opcje zdefiniowane przez wiadomości zdefiniowane w google/protobuf/descriptor.proto (np. FileOptions lub FieldOptions), zdefiniowanie własnych opcji jest po prostu rozszerzaniem tych wiadomości. Przykład:

import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

Zdefiniowaliśmy nową opcję na poziomie wiadomości, rozszerzając właściwość MessageOptions. Jeśli użyjemy tej opcji, nazwa opcji musi być ujęta w nawiasy, aby wskazać, że jest to rozszerzenie. Wartość parametru my_option możemy teraz odczytywać w C++ w ten sposób:

string value = MyMessage::descriptor()->options().GetExtension(my_option);

Tutaj MyMessage::descriptor()->options() zwraca komunikat protokołu MessageOptions dla MyMessage. Odczytywanie z nich opcji niestandardowych wygląda tak samo jak odczytywanie innych rozszerzeń.

W języku Java wygląda to tak:

String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
  .getExtension(MyProtoFile.myOption);

W Pythonie będzie to:

value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
  .Extensions[my_proto_file_pb2.my_option]

Opcje niestandardowe możesz zdefiniować dla każdego typu konstrukcji w języku bufora protokołu. Oto przykład użycia każdej z tych opcji:

import "google/protobuf/descriptor.proto";

extend google.protobuf.FileOptions {
  optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
  optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
  optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
  optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
  optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50007;
}

option (my_file_option) = "Hello world!";

message MyMessage {
  option (my_message_option) = 1234;

  optional int32 foo = 1 [(my_field_option) = 4.5];
  optional string bar = 2;
  oneof qux {
    option (my_oneof_option) = 42;

    string quux = 3;
  }
}

enum MyEnum {
  option (my_enum_option) = true;

  FOO = 1 [(my_enum_value_option) = 321];
  BAR = 2;
}

message RequestType {}
message ResponseType {}

service MyService {
  option (my_service_option) = FOO;

  rpc MyMethod(RequestType) returns(ResponseType) {
    // Note:  my_method_option has type MyMessage.  We can set each field
    //   within it using a separate "option" line.
    option (my_method_option).foo = 567;
    option (my_method_option).bar = "Some string";
  }
}

Pamiętaj, że jeśli chcesz użyć opcji niestandardowej w pakiecie innym niż ten, w którym została zdefiniowana, musisz ją nazwać prefiksem, tak samo jak w przypadku nazw typów. Przykład:

// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
  option (foo.my_option) = "Hello world!";
}

Jeszcze jedno: opcje niestandardowe to rozszerzenia, więc trzeba im przypisać numery pól tak samo jak w przypadku innych pól lub rozszerzeń. W powyższych przykładach zastosowaliśmy numery pól w zakresie 50 000–99999. Ten zakres jest zarezerwowany do użytku wewnętrznego w poszczególnych organizacjach, więc możesz używać numerów z tego zakresu we własnych aplikacjach. Jeśli jednak zamierzasz używać opcji niestandardowych w aplikacjach publicznych, upewnij się, że numery pól są unikalne globalnie. Aby uzyskać globalnie unikalne numery pól, wyślij prośbę o dodanie wpisu do rejestru rozszerzeń globalnych protokołu protobuf. Zwykle wystarczy jeden numer rozszerzenia. Możesz zadeklarować wiele opcji tylko z jednym numerem rozszerzenia, umieszczając je w wiadomości podrzędnej:

message FooOptions {
  optional int32 opt1 = 1;
  optional string opt2 = 2;
}

extend google.protobuf.FieldOptions {
  optional FooOptions foo_options = 1234;
}

// usage:
message Bar {
  optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
  // alternative aggregate syntax (uses TextFormat):
  optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}

Pamiętaj też, że każdy typ opcji (na poziomie pliku, komunikatu, na poziomie pola itd.) ma własną przestrzeń liczbową, więc możesz na przykład zadeklarować, że rozszerzenia Pola Opcje i MessageOptions mają tę samą liczbę.

Generuję zajęcia

Aby wygenerować kod w języku Java, Python lub C++ do obsługi typów wiadomości zdefiniowanych w pliku .proto, musisz uruchomić kompilator protoc bufora protokołu w .proto. Jeżeli kompilator nie jest jeszcze zainstalowany, pobierz go i postępuj zgodnie z instrukcjami w README.

Kompilator protokołu jest wywoływany w ten sposób:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
  • IMPORT_PATH określa katalog, w którym należy szukać plików .proto przy przetwarzaniu dyrektyw import. W przypadku jego pominięcia używany jest bieżący katalog. Aby przekazać kilka katalogów importu, możesz przekazać kilka razy opcję --proto_path. Będą one stosowane kolejno. -I=_IMPORT_PATH_ może być używany jako krótka postać --proto_path.
  • Możesz dodać co najmniej jedną dyrektywę wyjściową:

    Dla ułatwienia, jeśli instrukcja DST_DIR kończy się tekstem .zip lub .jar, kompilator zapisuje dane wyjściowe w jednym archiwum w formacie ZIP o podanej nazwie. Dodatkowo do danych wyjściowych .jar zostanie przesłany plik manifestu zgodny ze specyfikacją Java JAR. Jeśli archiwum danych wyjściowych już istnieje, zostanie zastąpione, ponieważ kompilator nie jest na tyle zaawansowany, by dodać pliki do istniejącego archiwum.

  • Musisz podać co najmniej 1 plik .proto. Możesz określić wiele plików .proto jednocześnie. Mimo że nazwy plików odnoszą się do bieżącego katalogu, każdy z nich musi być zapisany w jednym z tych plików (IMPORT_PATH), by kompilator mógł określić nazwę kanoniczną tego pliku.

Lokalizacja pliku

Wolę nie umieszczać plików .proto w tym samym katalogu co w innych językach. Rozważ utworzenie podpakietu proto na pliki .proto w katalogu głównym projektu.

Lokalizacja nie zależy od języka

Podczas pracy z kodem Java dobrze jest umieścić powiązane pliki .proto w tym samym katalogu co źródło Java. Jeśli jednak kod w języku innym niż Java nie będzie korzystał z tych samych proto, prefiks ścieżki nie będzie już miał sensu. Zasadniczo umieść proto w powiązanym katalogu niezależnym od języka, np. //myteam/mypackage.

Wyjątkiem od tej zasady jest sytuacja, w której jasne jest, że obiekty proto będą używane tylko w kontekście Java, na przykład do celów testowych.

Obsługiwane platformy

Informacje o: