- Definiowanie typu wiadomości
- Typy wartości skalarnych
- Pola opcjonalne i wartości domyślne
- Wyliczenia
- Korzystanie z innych typów wiadomości
- Typy zagnieżdżone
- Aktualizowanie typu wiadomości
- Rozszerzenia
- Jeden
- Mapy
- Przesyłki
- Definiowanie usług
- Opcje
- Generowanie zajęć
- Lokalizacja
- Obsługiwane platformy
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 klasyBuilder
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
lubrepeated
. 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ówrequired
. 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
ibool
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
isint64
są ze sobą zgodne, ale nie są zgodne z innymi typami liczb całkowitych. - Znaki
string
ibytes
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 zsfixed32
orazfixed64
zsfixed64
.- W przypadku pól
string
,bytes
i wiadomościoptional
jest zgodny zrepeated
. 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 poleoptional
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 formatamiint32
,uint32
,int64
iuint64
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ścienum
są odrzucane, gdy wiadomość jest preferencyjna, przez co akcesorhas..
pola zwraca wartość fałsz, a metoda pobierania zwraca pierwszą wartość podaną w definicjienum
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 doenum
, 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 elementuoneof
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 interfejsuoneof
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 zasobuoneof
nie jest bezpieczne. I podobnie, zmiana jednego polaoneof
na pole lub rozszerzenieoptional
jest bezpieczna. - Zmiana pola między właściwością
map<K, V>
a odpowiadającym mu polemrepeated
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 polarepeated
otrzymają wynik semantyczny, ale klienci używający definicji polamap
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::kFirstReservedNumber
–FieldDescriptor::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 metodyset_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żejmsg1
będzie miałsub_message
, amsg2
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
anirequired
. - 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ędzieOpen
. - 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łago_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 opcjajava_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 elementujava_outer_classname
, nazwa klasy zostanie utworzona poprzez przekonwertowanie nazwy pliku.proto
na wielkość liter. W takim przypadku opcjajava_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 (patrzjava_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
lubLITE_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ż wSPEED
, ale operacje będą wolniejsze. Klasy będą nadal implementować dokładnie ten sam publiczny interfejs API, co w trybieSPEED
. 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
zamiastlibprotobuf
). Ś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 trybieSPEED
. Wygenerowane klasy będą implementować interfejsMessageLite
tylko w każdym języku, co zapewnia tylko podzbiór metod pełnego interfejsuMessage
.
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 totrue
. 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 nazwieMessageSet
. 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 poletrue
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 dyrektywimport
. 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ą:
--cpp_out
generuje kod C++ w aplikacjiDST_DIR
. Więcej informacji znajdziesz w dokumentacji dotyczącej kodu wygenerowanego przez C++.--java_out
generuje kod Java wDST_DIR
. Więcej informacji znajdziesz w dokumentacji kodu Java.--python_out
generuje kod w Pythonie wDST_DIR
. Więcej informacji znajdziesz w dokumentacji dotyczącej kodu wygenerowanej w języku Python.
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:
- obsługiwanych systemów operacyjnych, kompilatorów, systemów kompilacji oraz wersji C++ znajdziesz w Zasadach pomocy dotyczących podstaw C++.
- obsługiwanych wersji PHP, które znajdziesz w tym artykule.