Буферы протоколов (protobuf) — это независимый от языка и платформы расширяемый способ сериализации структурированных данных, аналогичный XML, но более гибкий и эффективный, чем XML. Хотя protobuf часто используется в повседневной работе, он часто используется только при использовании базовой грамматики, и многие расширенные функции и грамматики не полностью поняты.При чтении некоторых прото-библиотек с открытым исходным кодом вы всегда увидите некоторые грамматики, которые обычно не б/у. , влияющие на понимание.
Эта статья основана на языке Go и суммирует всеproto3
Общие и необычные грамматики и примеры помогут вам полностью понять грамматику protobuf, углубить свое понимание и преодолеть барьер чтения исходного кода.
Quick Start
Написано с использованием синтаксиса protobufxxx.proto
файл, который затем компилируется в файл кода, который может быть распознан и использован конкретным языком для вызовов программ, что является основным принципом работы protobuf.
Например, перейдите на язык, компилятор будет использовать официальныйxxx.proto
файл, скомпилированный вxxx.pb.go
файл - обычный файл кода Go.
Чтобы использовать protobuf, сначала нам нужно скачать компилятор protobuf — protoc, но язык Go напрямую не поддерживается компилятором, а на него ссылается компилятор через плагины, поэтому нам также нужно скачать плагин для компиляции языка Go:
- Загрузите компилятор для соответствующей среды (
protoc-$VERSION-$PLATFORM.zip
):Раздел GitHub.com/protocol… - Загрузите и установите плагин компилятора языка Go:
go install google.golang.org/protobuf/cmd/protoc-gen-go
После установки подготавливаем следующие файлы$SRC_DIR/quick_start.proto
:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
Выполните команду компилятора:protoc --go_out=$DST_DIR $SRC_DIR/quick_start.proto
.
Эта команда скомпилирует$SRC_DIR/quick_start.proto
файл и сохраните выходные данные компиляции на основе языка Go в файл$DST_DIR/quick_start.qb.go
середина:
....
type SearchRequest struct {
Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
PageNumber int32 `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"`
ResultPerPage int32 `protobuf:"varint,3,opt,name=result_per_page,json=resultPerPage,proto3" json:"result_per_page,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
....
Внедрение make-файлов в программуquick_start.qb.go
В пакете, где она находится, структуру можно сериализовать и десериализовать по способу protobuf.
Сериализация:
req := &pb.SearchRequest{} //此处pb是 quick_start.qb.go 所在包的别名
// ...
// 序列化结构体,写入文件
out, err := proto.Marshal(req)
if err != nil {
log.Fatalln("Failed to encode search request :", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write search request:", err)
}
Десериализовать:
// 从文件读取消息,并将其反序列化成结构体
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.SearchRequest{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse search request:", err)
}
A Bit of Everything
В примере быстрого запуска показано самое простое использование. Ниже мы передаем список, включающий всеproto3
Примеры синтаксиса, последовательно объясняющие синтаксис и функции protobuf.
Пример кода можно найти здесь:a_bit_of_everything.proto
Выполнить в корневом каталоге кодаprotoc --go_out=plugins=grpc:. a_bit_of_everything.proto
генерироватьxxx.pb.go
документ.
package
syntax = "proto3";
option go_package = "examplepb"; // 编译后的golang包名
package example.everything; // proto包名
...
В начале файла примера вы увидитеgo_package
иpackage
два объявления о пакетах, но эти дваpackage
Смыслы не те,package example.everything;
указывает текущий.proto
Имя пакета, в котором находится файл, похоже на язык Go.Под тем же именем пакета такое же имя не может быть определено.message
,enum
илиservice
.option go_package = "examplepb"
определяет уровень файлаoption
, используемый для указания имени скомпилированного пакета golang.
import
...
import "google/protobuf/any.proto";
import "google/protobuf/descriptor.proto";
//import "other.proto";
...
import
Он используется для представления других прото-файлов.Если определение других прото-файлов должно использоваться в текущем файле, его необходимоimport
Заходи, тогда по аналогииpackageName.MessageName
способ обращения к требуемому контенту, аналогичный языку Goimport
очень похожий. выполнить компиляциюprotoc
, необходимо добавить-I
параметры для указанияimport
Путь к файлу, например:protoc -I $GOPATH/src --go_out=. a_bit_of_everything.proto
Any.proto и descriptor.proto, представленные в примере, были встроены в протокол, поэтому параметр -I не нужно добавлять для компиляции этого примера.
Скалярные типы значений
прототип | Введите тип | Примечание |
---|---|---|
double | float64 | |
float | float | |
int32 | int32 | Кодирование отрицательных значений относительно неэффективно |
int64 | int64 | Кодирование отрицательных значений относительно неэффективно |
uint32 | uint32 | |
uint64 | uint64 | |
sint32 | int32 | Когда значение отрицательное, кодировка более эффективна, чем int32. |
sint64 | int64 | Когда значение отрицательное, кодировка более эффективна, чем int64. |
fixed32 | uint32 | Кодирование более эффективно, чем uint32, когда значение всегда больше 2^28. |
fixed64 | uint64 | Кодирование более эффективно, чем uint32, когда значение всегда больше 2^56. |
sfixed32 | int32 | |
sfixed64 | int64 | |
bool | bool | |
string | string | Это может быть только кодировка utf-8 или 7-битный текст ASCII, а длина не должна превышать 2^32. |
bytes | []byte | Последовательность байтов произвольной длины не более 2^32 |
сообщение сообщение
// 普通的message
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message
Может содержать несколько объявлений полей, каждое объявление поля должно содержать тип поля, имя поля и уникальный порядковый номер. Тип поля может быть скалярным, перечисляемым или другим.message
тип. Уникальный порядковый номер используется для идентификации позиции этого поля в двоичном коде сообщения.
также можно использовать
repeated
Чтобы изменить тип поля, см. Ниже подробностиrepeated
инструкция.
тип перечисления
...
// 枚举 enum
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_OK = 1;
STATUS_FAIL= 2;
STATUS_UNKNOWN = -1; // 不推荐有负数
}
...
пройти черезenum
Ключевое слово определяет тип перечисления.В protobuf перечисление имеет тип int32. Первое значение перечисления должно начинаться с 0. Если вы не хотите использовать 0 значений в своем коде, вы можете использовать первое значение сXXX_UNSPECIFIED
в качестве заполнителя. Поскольку тип перечисления фактически закодирован в кодировке типа int32 protobuf, не рекомендуется использовать отрицательные числа в типе перечисления.
XXX_UNSPECIFIED
Просто спецификация кода. Не влияет на поведение кода.
Зарезервированные поля и зарезервированные значения
// 保留字段
message ReservedMessage {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
// string abc = 2; // 编译报错
// string foo = 3; // 编译报错
}
// 保留枚举
enum ReservedEnum {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
// FOO = 0; // 编译报错
F = 0;
}
если мы положимmessage
Поля в удалены и могут быть повторно использованы последующими обновлениями. Ошибки кодека могут возникать, когда по сети запускаются как старые, так и новые определения прототипов. Например, есть две версии, старая и новая.Foo
:
// old version
message Foo {
string a = 1;
}
// new version
message Foo {
int32 a = 1;
}
Ошибка возникает, если новая версия proto используется для разбора старой версии сообщения, потому что новая версия proto попытаетсяa
Разбираем в int32, но на самом деле старая версия proto основана на строковом типеa
закодировано. protobuf предоставляетсяreserved
Ключевые слова, позволяющие избежать конфликта между старой и новой версиями:
// new version
message Foo {
reserved 1; // 标记第一个字段是保留的
int32 a = 2; // 序号从2开始,就不会与旧版本的string类型a冲突了
}
вложенный
// nested 嵌套message
message SearchResponse {
message Result {
string url = 1 ;
string title = 2;
}
enum Status {
UNSPECIFIED = 0;
OK = 1;
FAIL= 2;
}
Result results = 1;
Status status = 2;
}
message
Допускается несколько уровней вложенности,message
иenum
могут быть вложенными. вложенныйmessage
иenum
Текущий может не толькоmessage
используется и может также использоваться другимиmessage
Цитировать:
message OtherResponse {
SearchResponse.Result result = 1;
SearchResponse.Status status = 2;
}
составной тип
В дополнение к скалярным типам protobuf также предоставляет некоторые нескалярные типы, которые я называю составными типами в этой статье.
Составные типы не являются официально классифицированными категориями. Это концепция, которую эта статья обобщает для простоты понимания.
repeated
// repeated
message RepeatedMessage {
repeated SearchRequest requests = 1;
repeated Status status = 2;
repeated int32 number = 3;
}
repeated
может воздействовать наmessage
на тип переменной в . ТолькоСкалярный тип,тип перечисленияитип сообщениявозможноrepeated
ретушь.repeated
Указывает, что текущая измененная переменная может повторяться любое количество раз (включая 0 раз), что на самом деле является массивом переменной длины, представляющим текущий измененный тип, то есть язык Go.slice
:
// repeated
type RepeatedMessage struct {
Requests []*SearchRequest `protobuf:"bytes,1,rep,name=requests,proto3" json:"requests,omitempty"`
Status []Status `protobuf:"varint,2,rep,packed,name=status,proto3,enum=example.everything.Status" json:"status,omitempty"`
Number []int32 `protobuf:"varint,3,rep,packed,name=number,proto3" json:"number,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
map
message MapMessage{
map<string, string> message = 1;
map<string, SearchRequest> request = 2;
}
Кромеslice
, и конечноmap
. где тип ключа может бытьУдалитьdouble
,float
,bytes
за пределамиСкалярный тип значения может быть любым скалярным типом, типом перечисления и типом сообщения. протобуфmap
Он также используется после компиляции в язык Go.map
Представлять:
...
// map
type MapMessage struct {
Message map[string]string `protobuf:"bytes,1,rep,name=message,proto3" json:"message,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Request map[string]*SearchRequest `protobuf:"bytes,2,rep,name=request,proto3" json:"request,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
...
any
...
import "google/protobuf/any.proto";
...
message AnyMessage {
string message = 1;
google.protobuf.Any details = 2;
}
...
any
类型可以包含一个不需要指定类型的任意的序列化消息。 нужно использоватьany
Тип, нужноimport google/protobuf/any.proto
.any
Кодирование/декодирование поля типа реализуется средой выполнения каждого языка, например, в языке Go вы можете читать и писать такany
Поля типа:
...
import "github.com/golang/protobuf/ptypes"
...
func getSetAny() {
fmt.Println("getSetAny")
req := &examplepb.SearchRequest{
Query: "query",
}
// 将SearchRequest打包成Any类型
a, err := ptypes.MarshalAny(req)
if err != nil {
log.Println(err)
return
}
// 赋值
anyMsg := &examplepb.AnyMessage{
Message: "any message",
Details: a,
}
req = &examplepb.SearchRequest{}
// 从Any类型中还原proto消息
err = ptypes.UnmarshalAny(anyMsg.Details, req)
if err != nil {
log.Println(err)
}
fmt.Println(" any:", req)
}
one of
// one of
message OneOfMessage {
oneof test_oneof {
string m1 = 1;
int32 m2 =2;
}
}
Если сообщение содержит несколько полей, но одновременно можно задать только одно из этих полей, вы можете передатьoneof
для обеспечения такого поведения. правильноoneof
Установка любого из полей приведет к очистке других полей. Например, для приведенного выше примераtest_oneof
Поля могут быть либо m1 типа string, либо m2 типа int32. Чтение и запись в Gooneof
Пример выглядит следующим образом:
func getSetOneof() {
fmt.Println("getSetOneof")
oneof := &examplepb.OneOfMessage{
// 同一时间只能设值一个值
TestOneof: &examplepb.OneOfMessage_M1{
M1: "this is m1",
},
}
fmt.Println(" m1:", oneof.GetM1()) // this is m1
fmt.Println(" m2:", oneof.GetM2()) // 0
}
options & extensions
Я считаю, что большинство сусликов мало обращают внимания на обычное использование protobufoptions
, 80% разработок не нужно использовать напрямуюoptions
. Но options — это очень полезная функция, которая значительно улучшает расширяемость protobuf, и нам необходимо ее понять.options
На самом деле какой-то встроенный protobufmessage
Тип, который разделен на следующие уровни:
- параметры на уровне файла
- Уровень сообщения (параметры уровня сообщения)
- параметры на уровне поля
- уровень обслуживания (варианты обслуживания)
- уровень метода (параметры метода)
protobuf предоставляет некоторые встроенныеoptions
доступны, также предлагаются черезextend
ключевые слова, чтобы расширить этиoptions
, чтобы добавить пользовательскийoptions
цель.
существует
proto2
В грамматике,extend
можно применить к любомуmessage
, но вproto3
В грамматике,extend
Может действовать только в соответствии с этими определениямиoption
изmessage
- только для настройкиoption
.
options
Не меняет общего смысла объявления (например, int32 — это int32, он не меняет свой объявленный тип из-за параметра), но может повлиять на то, как он обрабатывается в определенных случаях. Например, мы можем использовать встроенныйdeprecated option
пометить поле какdeprecated
:
message Msg {
string foo = 1;
string bar = 2 [deprecated = true]; //标记为deprecated。
}
Когда нам нужно написать собственные плагины протокола, мы можем настроитьoptions
Предоставляет дополнительную информацию для компиляции плагинов. Например, предположим, что я хочу разработать плагин проверки прототипа, который генерируетxxx.Validate()
способ проверки достоверности сообщения, я могу настроитьoptions
предоставить необходимую информацию для генерации кода:
message Msg {
// required是自定义options,表示foo字段必须非空
string foo = 1; [required = true];
}
встроенныйoptions
можно определить вРаздел GitHub.com/protocol…найдено для каждого уровняoptions
соответствует одномуmessage
, соответственно:
- FileOptions - уровень файла
- MessageOptions — уровень сообщения
- FieldOptions — уровень поля
- ServiceOptions — уровень обслуживания
- MethodOptions - уровень метода
Эти уровни будут представлены один за другим в качестве примеров.options
, и как их расширитьoptions
.
уровень файла
...
option go_package = "examplepb"; // 编译后的golang包名
...
message extObj {
string foo_string= 1;
int64 bar_int=2;
}
// file options
extend google.protobuf.FileOptions {
string file_opt_string = 1001;
extObj file_opt_obj = 1002;
}
option (example.everything.file_opt_string) = "file_options";
option (example.everything.file_opt_obj) = {
foo_string: "foo"
bar_int:1
};
go_package
Нет сомнений, что protobuf встроен для указания имени скомпилированного пакета golang. Помимо использования встроенного, это можно сделать с помощьюextend
поля для расширения встроенногоFileOptions
Например, в приведенном выше примере мы добавили два новых варианта - типы строкиfile_opt_string
и тип extObjfile_opt_obj
. и черезoption
Ключевое слово устанавливает два параметра на уровне файла. В Go мы можем прочитать эти параметры так:
func getFileOptions() {
fmt.Println("file options:")
msg := &examplepb.MessageOption{}
md, _ := descriptor.MessageDescriptorProto(msg)
stringOpt, _ := proto.GetExtension(md.Options, examplepb.E_FileOptString)
objOpt, _ := proto.GetExtension(md.Options, examplepb.E_FileOptObj)
fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
fmt.Println(" string:", *stringOpt.(*string))
}
распечатать результат:
file options:
obj.foo_string: foo
obj.bar_int 1
string: file_options
уровень сообщения
// message options
extend google.protobuf.MessageOptions {
string msg_opt_string = 1001;
extObj msg_opt_obj = 1002;
}
message MessageOption {
option (example.everything.msg_opt_string) = "Hello world!";
option (example.everything.msg_opt_obj) = {
foo_string: "foo"
bar_int:1
};
string foo = 1;
}
Он аналогичен файловому уровню и не будет здесь повторяться. Пример чтения на языке Go:
func getMessageOptions() {
fmt.Println("message options:")
msg := &examplepb.MessageOption{}
_, md := descriptor.MessageDescriptorProto(msg)
objOpt, _ := proto.GetExtension(md.Options, examplepb.E_MsgOptObj)
stringOpt, _ := proto.GetExtension(md.Options, examplepb.E_MsgOptString)
fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
fmt.Println(" string:", *stringOpt.(*string))
}
Уровень поля
// field options
extend google.protobuf.FieldOptions {
string field_opt_string = 1001;
extObj field_opt_obj = 1002;
}
message FieldOption {
// 自定义的option
string foo= 1 [(example.everything.field_opt_string) = "abc",(example.everything.field_opt_obj) = {
foo_string: "foo"
bar_int:1
}];
// protobuf内置的option
string bar = 2 [deprecated = true];
}
Метод определения опции на уровне поля не используетсяoption
Ключевые слова в формате: разделенные запятыми массивы вида k=v, заключенные в []. В Go мы можем прочитать эти параметры так:
func getFieldOptions() {
fmt.Println("field options:")
msg := &examplepb.FieldOption{}
_, md := descriptor.MessageDescriptorProto(msg)
stringOpt, _ := proto.GetExtension(md.Field[0].Options, examplepb.E_FieldOptString)
objOpt, _ := proto.GetExtension(md.Field[0].Options, examplepb.E_FieldOptObj)
fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
fmt.Println(" string:", *stringOpt.(*string))
}
Ссылка на проект приложения:GitHub.com/it-koow/go-…go-proto-validators — это плагин компиляции прототипов для создания прототипов сообщений, которые могут проверять достоверность прото-сообщений.Он использует параметры уровня поля для определения правил проверки.
уровни сервиса и метода
// service & method options
extend google.protobuf.ServiceOptions {
string srv_opt_string = 1001;
extObj srv_opt_obj = 1002;
}
extend google.protobuf.MethodOptions {
string method_opt_string = 1001;
extObj method_opt_obj = 1002;
}
service ServiceOption {
option (example.everything.srv_opt_string) = "foo";
rpc Search (SearchRequest) returns (SearchResponse) {
option (example.everything.method_opt_string) = "foo";
option (example.everything.method_opt_obj) = {
foo_string: "foo"
bar_int: 1
};
};
}
Опции на уровне службы и метода также передаются черезoption
Он определяется ключевым словом, которое похоже на параметры уровня файла и уровня сообщения, и не будет описываться снова. Пример чтения на языке Go:
func getServiceOptions() {
fmt.Println("service options:")
msg := &examplepb.MessageOption{}
md, _ := descriptor.MessageDescriptorProto(msg)
srv := md.Service[1] // ServiceOption
stringOpt, _ := proto.GetExtension(srv.Options, examplepb.E_SrvOptString)
fmt.Println(" string:", *stringOpt.(*string))
}
func getMethodOptions() {
fmt.Println("method options:")
msg := &examplepb.MessageOption{}
md, _ := descriptor.MessageDescriptorProto(msg)
srv := md.Service[1] // ServiceOption
objOpt, _ := proto.GetExtension(srv.Method[0].Options, examplepb.E_MethodOptObj)
stringOpt, _ := proto.GetExtension(srv.Method[0].Options, examplepb.E_MethodOptString)
fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
fmt.Println(" string:", *stringOpt.(*string))
}
Ссылка на проект приложения:GitHub.com/Personal PC-Eco sys…
grpc-gateway выражает отношения преобразования из grpc в http, настраивая параметры для метода rpc, и управляет поведением генерации swagger с помощью параметров уровня файла и уровня обслуживания.
Ссылаться на
developer.Google.capable/номер протокола…
developer.Google.capable/номер протокола…
GitHub.com/it-koow/go-…
GitHub.com/Personal PC-Eco sys…