Полный анализ грамматики Protobuf

Go

Буферы протоколов (protobuf) — это независимый от языка и платформы расширяемый способ сериализации структурированных данных, аналогичный XML, но более гибкий и эффективный, чем XML. Хотя protobuf часто используется в повседневной работе, он часто используется только при использовании базовой грамматики, и многие расширенные функции и грамматики не полностью поняты.При чтении некоторых прото-библиотек с открытым исходным кодом вы всегда увидите некоторые грамматики, которые обычно не б/у. , влияющие на понимание.

Эта статья основана на языке Go и суммирует всеproto3Общие и необычные грамматики и примеры помогут вам полностью понять грамматику protobuf, углубить свое понимание и преодолеть барьер чтения исходного кода.

Quick Start

Написано с использованием синтаксиса protobufxxx.protoфайл, который затем компилируется в файл кода, который может быть распознан и использован конкретным языком для вызовов программ, что является основным принципом работы protobuf.

Например, перейдите на язык, компилятор будет использовать официальныйxxx.protoфайл, скомпилированный вxxx.pb.goфайл - обычный файл кода Go.
Чтобы использовать protobuf, сначала нам нужно скачать компилятор protobuf — protoc, но язык Go напрямую не поддерживается компилятором, а на него ссылается компилятор через плагины, поэтому нам также нужно скачать плагин для компиляции языка Go:

  1. Загрузите компилятор для соответствующей среды (protoc-$VERSION-$PLATFORM.zip):Раздел GitHub.com/protocol…
  2. Загрузите и установите плагин компилятора языка 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…