Skip to content

Protobuf

Sitio web oficial: Protocol Buffers | Google Developers

Introducción

Tutorial oficial: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers

Protocol Buffers es un mecanismo de serialización de datos estructurados extensible, independiente del lenguaje y del protocolo, abierto por Google en 2008. Es más rápido al desempacar y empaquetar, y se usa principalmente en la comunicación relacionada con RPC. Puede definir la forma estructurada de los datos, y luego puede usar código fuente generado especialmente para escribir y leer fácilmente datos estructurados desde y hacia varios flujos de datos, y usarlo en varios lenguajes. A continuación, Protocol Buffers se denominará uniformemente protobuf.

protobuf es bastante popular, especialmente en el ámbito de Go. gRPC lo utiliza como mecanismo de serialización para la transmisión de protocolos.

Sintaxis

Primero, veamos un ejemplo para ver cómo es aproximadamente un archivo protobuf. En general, su sintaxis es muy simple y puede dominarla en diez minutos. A continuación se muestra un ejemplo de un archivo llamado search.proto. La extensión de archivo de protobuf es .proto:

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
}

message SearchResult {
  string data = 1;
}

service SearchService {
  rpc Search(SearchRequest) returns(SearchResult);
}
  • La primera línea syntax = "proto3"; indica que se usa la sintaxis proto3. Por defecto se usa la sintaxis proto3.
  • message se declara de manera similar a una estructura y es la estructura básica en proto.
  • SearchRequest define tres campos. Cada campo tendrá un nombre y un tipo.
  • service define un servicio. Un servicio contiene uno o más interfaces rpc.
  • La interfaz rpc debe tener exactamente un parámetro y un valor de retorno. Sus tipos deben ser message, no pueden ser tipos básicos.

Además, tenga en cuenta que cada línea en un archivo proto debe terminar con un punto y coma.

Comentarios

El estilo de comentarios es exactamente el mismo que en Go.

protobuf
syntax = "proto3";

/* Comentario
 * Comentario */
message SearchRequest {
  string query = 1; //Comentario
  string number = 2;
}

Tipos

Los modificadores de tipo solo pueden aparecer en message, no pueden aparecer solos.

Tipos básicos

Tipo protoTipo Go
doublefloat64
floatfloat32
int32int32
int64int64
uint32uint32
uint64uint64
sint32int32
sint64int64
fixed32uint32
fixed64uint64
sfixed32int32
sfixed64int64
boolbool
stringstring
bytes[]byte

Arrays

Agregue el modificador repeated antes de un tipo básico para indicar que es un tipo de array, correspondiente al slice en Go.

protobuf
message Company {
  repeated string employee = 1;
}

map

El formato para definir un tipo map en protobuf es el siguiente:

map<key_type, value_type> map_field = N;

key_type debe ser numérico o cadena. value_type no tiene restricción de tipo. Vea un ejemplo:

protobuf
message Person {
  map<string, int64> cards = 1;
}

Campos

De hecho, proto no es un tipo tradicional de clave-valor. En el archivo proto declarado no aparecerán datos específicos. Después de cada = de campo debe seguir el número único actual en el message. Estos números se utilizan para identificar y definir estos campos en el cuerpo del mensaje binario. Los números comienzan desde 1. Los números del 1 al 15 ocupan 1 byte, y los del 16 al 2047 ocupan dos bytes. Por lo tanto, asigne los números del 1 al 15 a los campos que aparecen con frecuencia para ahorrar espacio, y debe dejar algo de espacio para los campos que puedan aparecer con frecuencia en el futuro.

Los campos en un message deben seguir las siguientes reglas:

  • singular: por defecto es un campo de ese tipo. En un message bien estructurado, solo puede haber 0 o 1 campo de este tipo, es decir, no puede haber el mismo campo repetido. La siguiente declaración causará un error:

    protobuf
    syntax = "proto3";
    
    message SearchRequest {
      string query = 1;
      string number = 2;
      string number = 3;//Campo repetido
    }
  • optional: similar a singular, solo que puede verificar explícitamente si se estableció el valor del campo. Puede haber las siguientes dos situaciones:

    • set: se serializará
    • unset: no se serializará
  • repeated: este tipo de campo puede aparecer 0 o más veces. Los valores duplicados se conservarán en orden (en otras palabras, es un array, puede permitir que el mismo tipo de valor aparezca varias veces y se conserve en el orden en que aparece, es un índice).

  • map: campo de tipo clave-valor. La forma de declaración es la siguiente:

    protobuf
    map<string,int32> config = 3;

Campos reservados

La palabra clave reserve puede declarar campos reservados. Después de declarar el número de campo reservado, ya no se puede usar como número y nombre de otros campos, y también ocurrirá un error durante la compilación. La respuesta oficial de Google es: si un archivo proto elimina algunos números en una nueva versión, entonces en el futuro otros usuarios pueden reutilizar estos números eliminados. Pero si vuelve a la versión anterior del número, causará un error porque el número correspondiente al campo es inconsistente. Los campos reservados pueden jugar un papel de recordatorio en el período de compilación, recordándole que no puede usar este campo reservado, de lo contrario la compilación no pasará.

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  reserved "a"; //Declarar campo con nombre específico como campo reservado
  reserved 1 to 2; //Declarar una secuencia de números como campo reservado
  reserved 3,4; //Declarar
}

De esta manera, este archivo no pasará la compilación.

Campos obsoletos

Si un campo está obsoleto, se puede escribir de la siguiente manera:

protobuf
message Body {
  string name = 1 [deprecated = true];
}

Enumeraciones

Puede declarar constantes de enumeración y usarlas como tipo de campo. Tenga en cuenta que el primer elemento del elemento de enumeración debe ser cero, porque el valor predeterminado del elemento de enumeración es el primer elemento.

protobuf
syntax = "proto3";

enum Type {
  GET = 0;
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

Cuando hay elementos de enumeración con el mismo valor dentro del elemento de enumeración, se puede usar un alias de enumeración:

protobuf
syntax = "proto3";

enum Type {
  option allow_alias = true; //Necesita habilitar la opción de configuración que permite usar alias
  GET = 0;
  GET_ALIAS = 0; //Alias del elemento de enumeración GET
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

Mensajes anidados

protobuf
message Outer {                  // Nivel 0
  message MiddleAA {  // Nivel 1
    message Inner {   // Nivel 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Nivel 1
    message Inner {   // Nivel 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

message puede anidar la declaración de message, como anidar estructuras.

Package

Puede agregar un modificador de paquete opcional a un archivo protobuf para evitar conflictos de nombres entre tipos de mensajes de protocolo.

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

Luego, puede usar el nombre del paquete al definir campos en el tipo de mensaje:

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

Import

La importación permite que múltiples archivos protobuf compartan definiciones. Su sintaxis es la siguiente. No se puede omitir la extensión del archivo al importar.

protobuf
import "a/b/c.proto";

Al importar, se usan rutas relativas. Esta ruta relativa no se refiere a la ruta relativa entre el archivo importado y el archivo importado, sino que depende de la ruta de escaneo especificada cuando el compilador protoc genera código. Suponga que tiene la siguiente estructura de archivo:

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

Si solo necesitamos generar código para la parte del directorio player, y solo especificamos el directorio player al escanear la ruta, entonces las importaciones mutuas entre health.proto y player.proto pueden escribir directamente el nombre de archivo único. Por ejemplo, player.proto importa health.proto:

protobuf
import "health.proto";

Si en este momento player.proto importa common.proto o archivos en el directorio monster, la compilación fallará. Por lo tanto, la siguiente escritura es completamente incorrecta, porque el compilador no puede encontrar estos archivos:

go
import "../common.proto"; // Escritura incorrecta

TIP

Dicho sea de paso, los símbolos .. y . no están permitidos en la ruta de importación.

Suponga que especifica pb_learn como ruta de escaneo durante la compilación. Entonces puede importar archivos de otros directorios a través de rutas relativas. La ruta de importación real es la dirección relativa de la dirección absoluta del archivo relativa a pb_learn. Vea el siguiente ejemplo de player.proto importando otros archivos:

protobuf
import "common.proto";
imrpot "monster/monster.proto";
import "player/health.proto";

Incluso health.proto en el mismo directorio debe usar una ruta relativa en este momento. Por lo tanto, en un proyecto, generalmente creamos una carpeta separada para almacenar todos los archivos protobuf y la especificamos como ruta de escaneo durante la compilación. Y todos los comportamientos de importación en este directorio también se basan en su ruta relativa.

TIP

Si usa el editor Goland, para su directorio protobuf creado por usted mismo, no se puede analizar de forma predeterminada, lo que resultará en un error. Si quiere que Goland lo reconozca, debe establecer manualmente la ruta de escaneo. El principio es exactamente el mismo que se describió anteriormente. El método de configuración es el siguiente: abra la siguiente configuración:

File | Settings | Languages & Frameworks | Protocol Buffers

Agregue manualmente la ruta de escaneo en Import Paths. Esta ruta de escaneo debe ser consistente con la ruta especificada durante la compilación.

Any

El tipo Any le permite usar mensajes como tipos incrustados sin necesidad de sus definiciones proto. Podemos importar directamente los tipos definidos por Google. Viene incorporado y no necesita escribirlo manualmente.

protobuf
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

Google también ha predefinido muchos otros tipos. Visite protobuf/ptypes at master · golang/protobuf (github.com) para ver más. Principalmente incluyen:

  • Encapsulamiento de tipos básicos
  • Tipo de tiempo
  • Tipo Duration

Las definiciones protobuf de ellos deben estar en el directorio include del compilador protoc.

OneOf

La explicación dada por la documentación oficial aquí es demasiado engorrosa. En términos simples, significa que un campo puede tener múltiples tipos posibles durante la transmisión, pero finalmente solo se puede usar un tipo. No se permiten campos modificados por repeated dentro de él. Es como el union en el lenguaje C.

protobuf
message Stock {
    // Datos específicos de Stock
}

message Currency {
    // Datos específicos de Currency
}

message ChangeNotification {
  int32 id = 1;
  oneof instrument {
    Stock stock = 2;
    Currency currency = 3;
  }
}

Service

La palabra clave service puede definir un servicio RPC. Un servicio RPC contiene varias interfaces rpc. Las interfaces se dividen en interfaces unarias e interfaces de flujo.

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  rpc DoSomething(Body) returns(Body);
}

Las interfaces de flujo se dividen en flujo unidireccional y flujo bidireccional. Generalmente se modifican con la palabra clave stream. Vea el siguiente ejemplo:

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  // Flujo del lado del cliente
  rpc DoSomething(stream Body) returns(Body);
  // Flujo del lado del servidor
  rpc DoSomething1(Body) returns(stream Body);
  // Flujo bidireccional
  rpc DoSomething2(stream Body) returns(stream Body);
}

El llamado flujo es enviar datos mutuamente a largo plazo en una conexión, ya no es una simple pregunta y respuesta como en la interfaz unaria.

Empty

empty es en realidad un message vacío, correspondiente a una estructura vacía en Go. Rara vez se usa para modificar campos. Se usa principalmente para indicar que una interfaz rpc no requiere parámetros o no tiene valor de retorno.

protobuf
syntax = "proto3";

import "google/protobuf/empty.proto";

service EmptyService {
  rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}

Option

option se usa generalmente para controlar algunos comportamientos de protobuf. Por ejemplo, para controlar el paquete generado por el código fuente del lenguaje Go, se puede declarar de la siguiente manera:

protobuf
option go_package = "github/jack/sample/pb_learn;pb_learn"

Lo que está antes del punto y coma es la ruta de importación de otros archivos fuente después de que se genera el código, y lo que está después del punto y coma es el nombre del paquete del archivo generado correspondiente.

Puede hacer algunas optimizaciones. Hay los siguientes valores disponibles y no se pueden declarar repetidamente:

  • SPEED: el grado de optimización más alto, el volumen de código generado es el más grande. Este es el predeterminado.
  • CODE_SIZE: reducirá el volumen de generación de código, pero dependerá de la reflexión para la serialización.
  • LITE_RUNTIME: el volumen de código es el más pequeño, pero faltan algunas características.

A continuación se muestra un caso de uso:

protobuf
option optimize_for = SPEED;

Además, option también puede agregar algo de metainformación a message y enum. Esta información se puede obtener mediante reflexión, lo cual es particularmente útil al realizar la validación de parámetros.

Compilación

La compilación es la generación de código. Arriba solo se definió el archivo protobuf. Cuando se usa realmente, debe convertirse en código fuente de un lenguaje específico para usarlo. Completamos esto a través del compilador protoc, que admite múltiples lenguajes.

Instalación

Para descargar el compilador, vaya a protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) para descargar la última versión de Release. Generalmente es un archivo comprimido:

protoc-25.1-win64
│  readme.txt

├─bin
│      protoc.exe

└─include
    └─google
        └─protobuf
            │  any.proto
            │  api.proto
            │  descriptor.proto
            │  duration.proto
            │  empty.proto
            │  field_mask.proto
            │  source_context.proto
            │  struct.proto
            │  timestamp.proto
            │  type.proto
            │  wrappers.proto

            └─compiler
                    plugin.proto

Después de descargar, agregue el directorio bin al PATH para poder usar el comando protoc. Después de completar, verifique la versión. Si puede salir normalmente, significa que la instalación fue exitosa:

bash
$ protoc --version
libprotoc 25.1

El compilador descargado no admite el lenguaje Go de forma predeterminada, porque la generación de código del lenguaje Go es un archivo ejecutable separado, y otros lenguajes están todos juntos. Entonces instale el complemento del lenguaje Go para traducir la definición protobuf al código fuente del lenguaje Go.

bash
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Si también necesita generar código de servicio gRPC, instale el siguiente complemento:

bash
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Después de la instalación, verifique su versión:

bash
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0

$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0

Estos complementos también son archivos binarios independientes, pero solo se pueden llamar a través de protoc y no se pueden ejecutar por separado.

(this program should be run by protoc, not directly)

Además, hay muchos otros complementos, como complementos para generar documentación de interfaz openapi, etc. Si está interesado, puede buscarlos usted mismo.

Generación

Todavía tomemos el ejemplo anterior. La estructura es la siguiente:

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

Para generar código, se deben especificar un total de tres parámetros:

  1. Ruta de escaneo: indica al compilador dónde buscar archivos protobuf y cómo analizar rutas de importación.
  2. Ruta de generación: dónde se colocan los archivos compilados.
  3. Archivo objetivo: especifica qué archivos objetivo deben compilarse.

Antes de comenzar, asegúrese de que go_package en el archivo protobuf esté configurado correctamente. Use protoc -h para ver los parámetros admitidos. Los más comunes son -I o --proto_path, que se pueden usar varias veces para especificar múltiples rutas de escaneo. Por ejemplo:

bash
$ protoc --proto_path=./pb_learn --proto_path=./third_party

Solo especificar la ruta de escaneo no es suficiente. También debe especificar la ruta de generación y el archivo protobuf objetivo. Aquí se generan archivos go, por lo que se usa el parámetro --go_out, soportado por el complemento protoc-gen-go descargado anteriormente:

bash
$ cd pb_learn

$ protoc --proto_path=. --go_out=. common.proto

$ ls
common.pb.go  common.proto  monster/  player/

El parámetro --go_out especifica la ruta de generación. . significa la ruta actual. common.proto es el archivo a compilar. Si desea generar código grpc (siempre que tenga el complemento grpc instalado), puede agregar el parámetro --go-grpc_out (si no hay definición de service en el archivo protobuf, no se generará el archivo correspondiente):

bash
$ protoc --proto_path=. --go_out=. --go-grpc_out=. common.proto

$ ls
common.pb.go  common.proto  common_grpc.pb.go  monster/  player/

common.pb.go es la definición de tipo protobuf generada. common_grpc.pb.go es el código gRPC generado. Se basa en el primero. Si no hay definición de lenguaje correspondiente, no se puede generar código gRPC.

Si desea compilar todos los archivos protobuf en el directorio, puede usar el comodín *, como *.proto:

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

Si desea incluir todos los archivos, puede usar el comodín **, como ./**/*.proto:

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto

Sin embargo, este método solo es aplicable a shells que admiten este tipo de comodines. Por ejemplo, en Windows, ni cmd ni powershell admiten esta escritura:

powershell
D> protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"

Afortunadamente, gitbash admite muchos comandos de Linux y también puede hacer que Windows admita esta sintaxis. Para evitar tener que escribir comandos repetidos cada vez, puede colocarlos en un makefile:

makefile
.PHONY: all

proto_gen:
  protoc --proto_path=. \
       --go_out=paths=source_relative:. \
       --go-grpc_out=paths=source_relative:. \
       ./**/*.proto ./*.proto

Se puede notar que se agrega paths=source_relative:.. Esto establece el modo de ruta de generación de archivos. Hay las siguientes opciones disponibles:

  • paths=import: este es el predeterminado. El archivo se generará en el directorio especificado por import. También puede ser una ruta de módulo. Por ejemplo, si hay un archivo protos/buzz.proto y se especifica paths=example.com/project/protos/fizz, finalmente se generará example.com/project/protos/fizz/buzz.pb.go.
  • module=$PREFIX: al generar, se eliminará el prefijo de ruta. En el ejemplo anterior, si se especifica el prefijo example.com/project, finalmente se generará protos/fizz/buzz.pb.go. Este modo se usa principalmente para generarlo directamente en el módulo (parece que no hay mucha diferencia).
  • paths=source_relative: el archivo generado mantendrá la misma estructura relativa que el archivo protobuf en el directorio especificado.

Después del dos puntos : está la ruta de generación especificada.

|  common.proto
|  common.pb.go

├─monster
│      monster.pb.go
│      monster.proto

└─player
        health.pb.go
        health.proto
        health_grpc.pb.go
        player.pb.go
        player.proto

Reflexión

Puede extender enum y message a través de options. Primero importe "google/protobuf/descriptor.proto":

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Integer {
  INT64 = 0[
    (string_name) = "int_64"
  ];
}

Esto equivale a agregar metainformación al valor de enumeración. Lo mismo es válido para message:

protobuf
import "google/protobuf/descriptor.proto";

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

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

Esto equivale a tener reflexión sobre protobuf. Después de generar el código, se puede acceder a través de Descriptor:

go
func main() {
  message := pb_learn.MyMessage{}
  message.ProtoReflect().Descriptor().Options().ProtoReflect().Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
    fmt.Println(descriptor.FullName(), ":", value)
    return true
  })
}

Salida:

my_option:"Hello world!"

Esta forma se puede comparar con agregar tag a una estructura en Go. Es una sensación similar. Según esta forma, también se puede implementar la función de validación de parámetros. Solo necesita escribir reglas en options y verificar a través de Descriptor.

Golang editado por www.golangdev.cn