- Definición de un tipo de mensaje
- Tipos de valores escalares
- Campos opcionales y valores predeterminados
- Enumeraciones
- Usa otros tipos de mensajes
- Tipos anidados
- Actualiza un tipo de mensaje
- Extensiones
- Uno
- Maps
- Paquetes
- Definición de servicios
- Opciones
- Genera tus clases
- Ubicación
- Plataformas compatibles
En esta guía, se describe cómo usar el lenguaje del búfer de protocolo para estructurar los datos del búfer de protocolo, incluida la sintaxis del archivo .proto
y cómo generar clases de acceso a los datos a partir de los archivos .proto
. Abarca la versión proto2 del lenguaje del búfer de protocolo: para obtener información sobre la sintaxis de proto3, consulta la Guía del lenguaje Proto3.
Esta es una guía de referencia. Para ver un ejemplo paso a paso que usa muchas de las funciones descritas en este documento, consulta el instructivo del lenguaje que elegiste.
Definición de un tipo de mensaje
Primero veamos un ejemplo muy simple. Supongamos que deseas definir un formato de mensaje de solicitud de búsqueda, en el que cada solicitud de búsqueda tiene una string de consulta, la página en particular de los resultados que te interesan y una cantidad de resultados por página. Este es el archivo .proto
que usas para definir el tipo de mensaje.
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
La definición del mensaje SearchRequest
especifica tres campos (pares nombre/valor), uno para cada dato que deseas incluir en este tipo de mensaje. Cada campo tiene un nombre y un tipo.
Especifica los tipos de campo
En el ejemplo anterior, todos los campos son tipos escalares: dos números enteros (page_number
y result_per_page
) y una string (query
). Sin embargo, también puedes especificar tipos compuestos para tus campos, incluidas enumeraciones y otros tipos de mensajes.
Asignación de números de campo
Como puedes ver, cada campo en la definición del mensaje tiene un número único. Estos números se usan para identificar tus campos en el formato binario de mensaje y no se deben cambiar una vez que el tipo de mensaje está en uso. Los números de campo en el rango 1 a 15 toman un byte a codificar, que incluye el número de campo y el tipo de campo (puedes encontrar más información sobre esto en Codificación de búfer de protocolo). Los números de campo en el rango del 16 al 2047 toman dos bytes. Por lo tanto, debes reservar los números de campo del 1 al 15 para los elementos de mensaje muy frecuentes. Recuerda dejar espacio para los elementos frecuentes que podrían agregarse en el futuro.
El número de campo más pequeño que puedes especificar es 1 y el más grande es 229 - 1 o 536,870,911. Tampoco puedes usar los números del 19 000 al 19999 (de FieldDescriptor::kFirstReservedNumber
a FieldDescriptor::kLastReservedNumber
), ya que están reservados para la implementación del búfer de protocolo. El compilador del búfer de protocolo se quejará si usas uno de estos números reservados en tu .proto
. Del mismo modo, no puedes usar números de campo reservados anteriormente.
Especificación de reglas de campo
Especifica que los campos de mensaje son uno de los siguientes:
required
: Un mensaje bien formado debe tener exactamente uno de este campo.optional
: un mensaje con formato correcto puede tener cero o uno de este campo (pero no más de uno).repeated
: Este campo se puede repetir cualquier cantidad de veces (incluido cero) en un mensaje con formato correcto. Se conservará el orden de los valores repetidos.
Por razones históricas, los campos repeated
de tipos numéricos escalares (por ejemplo, int32
, int64
, enum
) no se codifican de manera tan eficiente como podrían. El código nuevo debe usar la opción especial [packed = true]
para obtener una codificación más eficiente. Por ejemplo:
repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];
Puedes obtener más información sobre la codificación packed
en Codificación de búfer de protocolo.
Obligatorio es para siempre Debes tener mucho cuidado cuando marcas campos como required
. Si en algún momento quieres dejar de escribir o enviar un campo obligatorio, será difícil cambiar el campo a un campo opcional. Los lectores antiguos considerarán que los mensajes sin este campo están incompletos y pueden rechazarlos o descartarlos de manera no intencional. En su lugar, considera escribir rutinas de validación personalizadas específicas de la aplicación para los búferes.
Cuando alguien agrega un valor a la enumeración, aparece un segundo problema con los campos obligatorios. En este caso, el valor enum no reconocido se trata como si faltara, lo que también causa que la verificación de valor requerida falle.
Agregar más tipos de mensajes
Se pueden definir varios tipos de mensajes en un solo archivo .proto
. Esto es útil si quieres definir varios mensajes relacionados; por ejemplo, si deseas definir el formato del mensaje de respuesta que corresponde a tu tipo de mensaje SearchResponse
, puedes agregarlo al mismo .proto
:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
Combinar mensajes genera un sobredimensionamiento Si bien se pueden definir varios tipos de mensajes (como mensajes, enumeraciones y servicios) en un solo archivo .proto
, también se pueden generar aumentos excesivos de dependencias cuando se definen grandes cantidades de mensajes con distintas dependencias en un solo archivo. Se recomienda incluir la menor cantidad posible de tipos de mensajes por archivo .proto
.
Agregar comentarios
Para agregar comentarios a tus archivos .proto
, usa la sintaxis de estilo C/C++ //
y /* ... */
.
/* 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.
}
Campos reservados
Si actualizas un tipo de mensaje mediante la eliminación completa de un campo, o el envío de comentarios, los usuarios futuros pueden volver a usar el número de campo cuando realicen sus propias actualizaciones en el tipo. Esto puede causar problemas graves si más adelante se cargan versiones anteriores del mismo .proto
, por ejemplo, daños en los datos, errores de privacidad, etcétera. Una forma de asegurarte de que esto no suceda es especificar que los números de campo (o los nombres, que también pueden causar problemas para la serialización JSON) de tus campos borrados, sean reserved
. El compilador del búfer de protocolo se quejará si algún usuario futuro intenta usar estos identificadores de campo.
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
Los rangos de números de campo reservados son inclusivos (9 to 11
es igual a 9, 10,
11
). Ten en cuenta que no puedes combinar nombres de campos y números de campo en la misma declaración reserved
.
¿Qué se genera a partir de su .proto
?
Cuando ejecutas el compilador de búfer de protocolo en una .proto
, el compilador genera el código en el lenguaje elegido en el que deberás trabajar con los tipos de mensajes que describes en el archivo, incluida la obtención y configuración de valores de campo, la serialización de tus mensajes en una transmisión de salida y el análisis de tus mensajes desde una transmisión de entrada.
- Para C++, el compilador genera un archivo
.h
y.cc
a partir de cada.proto
, con una clase para cada tipo de mensaje descrito en tu archivo. - En el caso de Java, el compilador genera un archivo
.java
con una clase para cada tipo de mensaje, así como clasesBuilder
especiales a fin de crear instancias de clase de mensaje. - Python es un poco diferente: el compilador de Python genera un módulo con un descriptor estático de cada tipo de mensaje en tu
.proto
, que luego se usa con una metaclase para crear la clase de acceso a los datos de Python necesaria en el entorno de ejecución. - En el caso de Go, el compilador genera un archivo
.pb.go
con un tipo para cada tipo de mensaje en tu archivo.
Puedes obtener más información sobre cómo usar las API para cada idioma con este instructivo. Para obtener aún más detalles sobre la API, consulta la referencia de la API relevante.
Tipos de valores escalares
Un campo de mensaje escalar puede tener uno de los siguientes tipos: la tabla muestra el tipo especificado en el archivo .proto
y el tipo correspondiente en la clase generada de forma automática:
Tipo .proto | Notas | Tipo de C++ | Tipo de Java | Tipo de Python[2] | Tipo de Go |
---|---|---|---|---|---|
double | double | double | float | *float64 | |
float | float | float | float | *float32 | |
int32 | Usa codificación de longitud variable. Es ineficiente para codificar números negativos. Si es probable que tu campo tenga valores negativos, usa sint32. | int32 | int | int | *int32 |
int64 | Usa codificación de longitud variable. No es eficiente para codificar números negativos. Si es probable que tu campo tenga valores negativos, usa sint64 en su lugar. | int64 | long | int/long[3] | *int64 |
uint32 | Usa codificación de longitud variable. | uint32 | número entero[1] | int/long[3] | *intinta32 |
uint64 | Usa codificación de longitud variable. | uint64 | long[1] | int/long[3] | *uint64 |
sint32 | Usa codificación de longitud variable. Valor int firmado. Codifican de manera más eficaz números negativos que números int32 normales. | int32 | int | int | *int32 |
SinT64 | Usa codificación de longitud variable. Valor int firmado. Codifican de manera más eficaz los números negativos que los números int64 normales. | int64 | long | int/long[3] | *int64 |
Fijo32 | Siempre cuatro bytes. Es más eficiente que uint32 si los valores suelen ser mayores que 228. | uint32 | número entero[1] | int/long[3] | *intinta32 |
fijo64 | Siempre ocho bytes. Es más eficiente que uint64 si los valores suelen ser mayores que 256. | uint64 | long[1] | int/long[3] | *uint64 |
fijo32 | Siempre cuatro bytes. | int32 | int | int | *int32 |
fijas64 | Siempre ocho bytes. | int64 | long | int/long[3] | *int64 |
bool | bool | boolean | bool | *Booleano | |
string | Una string siempre debe contener texto codificado en UTF-8. | string | String | Unicode (Python 2) o str (Python 3) | *string |
bytes | Puede contener cualquier secuencia arbitraria de bytes. | string | ByteString | bytes | []byte |
Puedes obtener más información sobre cómo se codifican estos tipos cuando serializas tu mensaje en Codificación de búfer de protocolo.
[1] En Java, los números enteros de 32 bits y 64 bits sin firma se representan con sus equivalentes firmados, y el bit superior solo se almacena en el bit de signo.
[2] En todos los casos, la configuración de los valores de un campo realizará una verificación de tipo para asegurar que sea válida.
[3] Los números enteros de 62 bits o 32 bits sin firma se representan siempre como largos cuando se decodifican, pero pueden ser un número entero si se proporciona un número entero cuando se configura el campo. En todos los casos, el valor debe ajustarse al tipo representado cuando se configura. Consulta [2].
Campos opcionales y valores predeterminados
Como se mencionó anteriormente, los elementos de una descripción se pueden etiquetar como optional
.
Un mensaje bien formado puede o no contener un elemento opcional. Cuando se analiza un mensaje, si no contiene un elemento opcional, el acceso al campo correspondiente en el objeto analizado muestra el valor predeterminado para ese campo. El valor predeterminado se puede especificar como parte de la descripción del mensaje.
Por ejemplo, supongamos que deseas proporcionar un valor predeterminado de 10 para el valor result_per_page
de un SearchRequest
.
optional int32 result_per_page = 3 [default = 10];
Si no se especifica el valor predeterminado para un elemento opcional, en su lugar se usa un valor predeterminado específico del tipo: para las strings, el valor predeterminado es la string vacía. Para los bytes, el valor predeterminado es la string de bytes vacía. Para booleanos, el valor predeterminado es falso. Para los tipos numéricos, el valor predeterminado es cero. En el caso de las enumeraciones, el valor predeterminado es el primero que aparece en la definición de tipo de la enumeración. Esto significa que se debe tener cuidado al agregar un valor al comienzo de una lista de valores de enumeración. Consulta la sección Actualiza un tipo de mensaje para conocer los lineamientos sobre cómo cambiar las definiciones de forma segura.
Enumeraciones
Cuando defines un tipo de mensaje, es posible que desees que uno de sus campos solo tenga una de una lista de valores predefinida. Por ejemplo, supongamos que deseas agregar un campo corpus
para cada SearchRequest
, donde el corpus puede ser UNIVERSAL
, WEB
, IMAGES
, LOCAL
, NEWS
, PRODUCTS
o VIDEO
. Puedes hacerlo de manera muy simple si agregas un enum
a tu definición de mensajes: un campo con un tipo enum
solo puede tener uno de un conjunto específico de constantes como su valor (si intentas proporcionar un valor diferente, el analizador lo tratará como un campo desconocido).
En el siguiente ejemplo, agregamos un enum
llamado Corpus
con todos los valores posibles y un campo de tipo 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];
}
Puedes definir alias si asignas el mismo valor a diferentes constantes de enumeración.
Para ello, debes establecer la opción allow_alias
en true
. De lo contrario, el compilador del búfer de protocolo generará un mensaje de error cuando se encuentren alias.
Aunque todos los valores de alias son válidos durante la deserialización, el primer valor siempre se usa cuando se serializa.
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;
}
Las constantes del enumerador deben estar en el rango de un número entero de 32 bits. Dado que los valores enum
usan codificación varint en la conexión, los valores negativos son ineficientes y, por lo tanto, no se recomiendan. Puedes definir enum
en una definición de mensajes o fuera de ella. Estos enum
se pueden volver a usar en cualquier definición de mensajes en tu archivo .proto
. También puedes usar un tipo enum
declarado en un mensaje como el tipo de un campo en un mensaje diferente, mediante la sintaxis _MessageType_._EnumType_
.
Cuando ejecutas el compilador del búfer de protocolo en una .proto
que usa un enum
, el código generado tendrá una enum
correspondiente para Java o C++, o una clase especial EnumDescriptor
para Python que se usa con el fin de crear un conjunto de constantes simbólicas con valores de número entero en la clase generada en tiempo de ejecución.
Para obtener más información sobre cómo trabajar con los enum
de mensajes en tus aplicaciones, consulta la guía de código generado para el lenguaje que elegiste.
Valores reservados
Si actualizas un tipo de enumeración cuando quitas una entrada de enumeración o la comentas por completo, los usuarios futuros pueden volver a usar el valor numérico cuando realicen sus propias actualizaciones en el tipo. Esto puede causar problemas graves si más adelante se cargan versiones anteriores del mismo .proto
, por ejemplo, daños en los datos, errores de privacidad, etcétera. Una forma de asegurarte de que esto no suceda es especificar que los valores numéricos (o nombres, que también pueden causar problemas para la serialización JSON) de las entradas borradas son reserved
. El compilador del búfer de protocolo se quejará si algún usuario futuro intenta usar estos identificadores. Puedes especificar que tu rango de valores numéricos reservado suba hasta el valor máximo posible mediante la palabra clave max
.
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
Ten en cuenta que no puedes combinar nombres de campos y valores numéricos en la misma declaración reserved
.
Cómo usar otros tipos de mensajes
Puedes usar otros tipos de mensajes como tipos de campo. Por ejemplo, supongamos que deseas incluir mensajes Result
en cada mensaje SearchResponse
. Para ello, puedes definir un tipo de mensaje Result
en el mismo .proto
y, luego, especificar un campo de tipo Result
en SearchResponse
:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
Importar definiciones
En el ejemplo anterior, el tipo de mensaje Result
se define en el mismo archivo que SearchResponse
. ¿Qué sucede si el tipo de mensaje que deseas usar como tipo de campo ya está definido en otro archivo .proto
?
Puedes usar definiciones de otros archivos .proto
si los importas. Para importar las definiciones de .proto
, agrega una instrucción de importación en la parte superior del archivo:
import "myproject/other_protos.proto";
De forma predeterminada, solo puedes usar definiciones de archivos .proto
importados directamente.
Sin embargo, a veces es posible que debas mover un archivo .proto
a una ubicación nueva.
En lugar de mover el archivo .proto
directamente y actualizar todos los sitios de llamadas en un solo cambio, puedes colocar un archivo de marcador de posición .proto
en la ubicación anterior para reenviar todas las importaciones a la ubicación nueva mediante la noción import public
.
Ten en cuenta que la función de importación pública no está disponible en Java.
Las dependencias import public
pueden depender de forma transitiva en cualquier código que importe el proto que contenga la declaración import public
. Por ejemplo:
// 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
El compilador de protocolos busca archivos importados en un conjunto de directorios especificados en la línea de comandos del compilador del protocolo mediante la marca -I
/--proto_path
. Si no se proporcionó ninguna marca, se ve en el directorio en el que se invocó el compilador. En general, debes establecer la marca --proto_path
en la raíz de tu proyecto y usar nombres completos para todas las importaciones.
Usa tipos de mensajes proto3
Es posible importar tipos de mensajes proto3 y usarlos en tus mensajes proto2, y viceversa. Sin embargo, las enumeraciones proto2 no se pueden usar en la sintaxis proto3.
Tipos anidados
Puedes definir y usar tipos de mensajes dentro de otros tipos de mensajes, como en el siguiente ejemplo: aquí el mensaje Result
se define dentro del mensaje SearchResponse
:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
Si deseas volver a usar este tipo de mensaje fuera de su tipo de mensaje superior, debes consultarlo como _Parent_._Type_
:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
Puedes anidar mensajes tan profundamente como quieras. En el siguiente ejemplo, ten en cuenta que los dos tipos anidados llamados Inner
son completamente independientes, ya que se definen dentro de mensajes diferentes:
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;
}
}
}
Grupos
Ten en cuenta que la función de grupos dejó de estar disponible y no debes usarla cuando crees tipos de mensajes nuevos. En su lugar, usa tipos de mensajes anidados.
Los grupos son otra forma de anidar información en las definiciones de mensajes. Por ejemplo, otra forma de especificar un SearchResponse
que contiene una cantidad de Result
es la siguiente:
message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
Un grupo simplemente combina un tipo de mensaje anidado y un campo en una sola declaración. En tu código, puedes tratar este mensaje como si tuviera un campo de tipo Result
llamado result
(el último nombre se convierte en minúsculas para que no entre en conflicto con el primero). Por lo tanto, este ejemplo es exactamente equivalente a SearchResponse
anterior, excepto que el mensaje tiene un formato de conexión diferente.
Cómo actualizar un tipo de mensaje
Si un tipo de mensaje existente ya no satisface todas tus necesidades (por ejemplo, te gustaría que el formato de mensaje tenga un campo adicional), pero de todos modos quieres usar el código creado con el formato anterior, no te preocupes. Es muy simple actualizar los tipos de mensajes sin romper el código existente cuando usas el formato de cable binario.
Si usas el formato de cable binario, verifica las siguientes reglas:
- No cambies los números de campo de los campos existentes.
- Los campos nuevos que agregues deben ser
optional
orepeated
. Esto significa que los mensajes serializados por código con tu formato de mensaje "anterior" se pueden analizar en función del nuevo código generado, ya que no faltarán elementosrequired
. Debes establecer valores predeterminados razonables para estos elementos a fin de que el código nuevo pueda interactuar de forma adecuada con los mensajes generados por el código antiguo. Del mismo modo, los mensajes creados por el código nuevo se pueden analizar con el código anterior: los objetos binarios antiguos simplemente ignoran el campo nuevo cuando se realiza un análisis. Sin embargo, los campos desconocidos no se descartan y, si el mensaje se serializa en otro momento, los campos desconocidos se serializan con él, por lo que, si el mensaje se pasa a un código nuevo, los campos nuevos todavía están disponibles. - Se pueden quitar los campos obligatorios, siempre y cuando el número de campo no se vuelva a usar en el tipo de mensaje actualizado. Es posible que quieras renombrar el campo como agregar el prefijo "OBSOLETE_", o hacer que el número de campo esté reservado para que los usuarios futuros de tu
.proto
no puedan volver a usar el número por accidente. - Un campo no obligatorio se puede convertir en una extensión y viceversa, siempre que el tipo y el número sean los mismos.
int32
,uint32
,int64
,uint64
ybool
son compatibles, lo que significa que puedes cambiar un campo de uno de estos tipos a otro sin tener que avanzar y volver a la compatibilidad con versiones anteriores. Si se analiza un número desde el cable que no cabe en el tipo correspondiente, obtendrás el mismo efecto que si convirtieras el número a ese tipo en C++ (por ejemplo, si un número de 64 bits se lee como un int32, se truncará a 32 bits).sint32
ysint64
son compatibles entre sí, pero no son compatibles con los otros tipos de números enteros.string
ybytes
son compatibles siempre que los bytes sean UTF-8 válidos.- Los mensajes incorporados son compatibles con
bytes
si los bytes contienen una versión codificada del mensaje. fixed32
es compatible consfixed32
yfixed64
consfixed64
.- Para
string
,bytes
y los campos de mensaje,optional
es compatible conrepeated
. Dados los datos serializados de un campo repetido como entrada, los clientes que esperan que este campo seaoptional
tomarán el último valor de entrada si es un campo de tipo primitivo o combinarán todos los elementos de entrada si es un campo de tipo de mensaje. Ten en cuenta que no es generalmente seguro para los tipos numéricos, incluidos los booleanos y las enumeraciones. Los campos repetidos de tipos numéricos se pueden serializar en el formato empaquetado, que no se analizarán correctamente cuando se espere un campooptional
. - Cambiar un valor predeterminado suele ser aceptable, siempre y cuando recuerdes que los valores predeterminados nunca se envían por cable. Por lo tanto, si un programa recibe un mensaje en el que no se configuró un campo en particular, el programa verá el valor predeterminado como se definió en la versión del protocolo de ese programa. NO verá el valor predeterminado que se definió en el código del remitente.
enum
es compatible conint32
,uint32
,int64
yuint64
en términos de formato de conexión (ten en cuenta que los valores se truncarán si no encajan). Sin embargo, ten en cuenta que el código del cliente puede tratarlos de manera diferente cuando se deserializa el mensaje. En particular, los valoresenum
no reconocidos se descartan cuando se deserializa el mensaje, lo que hace que el descriptor de acceso del campohas..
muestre falso y su captador muestre el primer valor que aparece en la definiciónenum
, o el valor predeterminado si se especifica uno. En el caso de los campos de enumeración repetidas, los valores no reconocidos se quitan de la lista. Sin embargo, un campo de número entero siempre conservará su valor. Debido a esto, debes tener mucho cuidado cuando actualices un número entero a unenum
en términos de recibir valores de enumeración fuera de los límites en el cable.- En las implementaciones actuales de Java y C++, cuando se quitan los valores
enum
no reconocidos, se almacenan junto con otros campos desconocidos. Ten en cuenta que el resultado puede ser un comportamiento extraño si los datos se serializan y, luego, se vuelven a analizar por un cliente que reconoce estos valores. En el caso de los campos opcionales, incluso si se escribió un valor nuevo después de deserializar el mensaje original, los clientes que aún lo reconocen podrán leer el valor anterior. En el caso de los campos repetidos, los valores anteriores aparecerán después de los valores reconocidos y agregados recientemente, lo que significa que no se conservará el orden. - Cambiar una sola extensión o campo
optional
por un miembro de unaoneof
nueva es compatible con objetos binarios; sin embargo, para algunos lenguajes (en particular, Go), la API del código generado cambiará de formas incompatibles. Por este motivo, Google no realiza estos cambios en sus API públicas, como se documenta en AIP-180. Con la misma advertencia sobre la compatibilidad de origen, mover varios campos a unoneof
nuevo puede ser seguro si estás seguro de que ningún código establece más de uno a la vez. Mover campos a unoneof
existente no es seguro. Del mismo modo, cambiar un solo campooneof
por un campooptional
o una extensión es seguro. - Cambiar un campo entre una
map<K, V>
y el campo de mensajerepeated
correspondiente es compatible con objetos binarios (consulta la sección Maps que se encuentra a continuación para conocer el diseño del mensaje y otras restricciones). Sin embargo, la seguridad del cambio depende de la aplicación: cuando se deserializan y vuelven a serializar un mensaje, los clientes que usan la definición del camporepeated
producirán un resultado semántico idéntico. Sin embargo, los clientes que usan la definición del campomap
pueden volver a ordenar las entradas y descartar las entradas con claves duplicadas.
Extensiones
Las extensiones te permiten declarar que un rango de números de campo en un mensaje está disponible para extensiones de terceros. Una extensión es un marcador de posición para un campo cuyo tipo no está definido por el archivo .proto
original. Esto permite que otros archivos .proto
se agreguen a tu definición de mensajes mediante la definición de los tipos de algunos o todos los campos con esos números de campo. Veamos un ejemplo:
message Foo {
// ...
extensions 100 to 199;
}
Esto indica que el rango de números de campo [100, 199] en Foo
está reservado para extensiones. Otros usuarios ahora pueden agregar campos nuevos a Foo
en sus propios archivos .proto
que importan tu .proto
mediante números de campo dentro del rango especificado, por ejemplo:
extend Foo {
optional int32 bar = 126;
}
Esto agrega un campo llamado bar
con el campo número 126 a la definición original de Foo
.
Cuando los mensajes Foo
de tu usuario están codificados, el formato de conexión es exactamente el mismo que si el usuario definiera el campo nuevo dentro de Foo
. Sin embargo, la forma en que accedes a los campos de extensiones en el código de tu aplicación es un poco diferente a acceder a los campos normales. Tu código de acceso a los datos generados tiene descriptores de acceso especiales para trabajar con extensiones. Por ejemplo, a continuación, se muestra cómo establecer el valor de bar
en C++:
Foo foo;
foo.SetExtension(bar, 15);
De manera similar, la clase Foo
define los descriptores de acceso de plantilla HasExtension()
, ClearExtension()
, GetExtension()
, MutableExtension()
y AddExtension()
. Todas tienen semántica que coincide con los descriptores de acceso correspondientes para un campo normal. Para obtener más información sobre cómo trabajar con extensiones, consulta la referencia del código generado para el idioma elegido.
Ten en cuenta que las extensiones pueden ser de cualquier tipo de campo, incluidos los tipos de mensajes, pero no pueden ser únicos ni mapas.
Extensiones anidadas
Puedes declarar extensiones en el alcance de otro tipo:
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
En este caso, el código de C++ para acceder a esta extensión es el siguiente:
Foo foo;
foo.SetExtension(Baz::bar, 15);
En otras palabras, el único efecto es que bar
se define dentro del alcance de Baz
.
Esta es una fuente común de confusión: declarar un bloque extend
anidado dentro de un tipo de mensaje no implica ninguna relación entre el tipo externo y el extendido. En particular, el ejemplo anterior no significa que Baz
sea algún tipo de subclase de Foo
. Todo lo que significa es que el símbolo bar
se declara dentro del alcance de Baz
; es simplemente un miembro estático.
Un patrón común es definir extensiones dentro del alcance del tipo de campo de la extensión; por ejemplo, esta es una extensión a Foo
de tipo Baz
, en la que la extensión se define como parte de Baz
:
message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
Sin embargo, no es necesario que una extensión con un tipo de mensaje se defina dentro de ese tipo. También puedes hacer lo siguiente:
message Baz {
...
}
// This can even be in a different file.
extend Foo {
optional Baz foo_baz_ext = 127;
}
De hecho, es posible que se prefiera usar esta sintaxis para evitar confusiones. Como se mencionó anteriormente, la sintaxis anidada suele confundirse con la subclasificación de los usuarios que aún no están familiarizados con las extensiones.
Cómo elegir los números de extensión
Es muy importante asegurarse de que dos usuarios no agreguen extensiones al mismo tipo de mensaje con el mismo número de campo. El daño a los datos puede producirse si una extensión se interpreta como un tipo incorrecto. Te recomendamos que definas una convención de numeración de extensiones para tu proyecto a fin de evitar que eso suceda.
Si tu convención de numeración podría implicar que las extensiones tengan números de campo muy grandes, puedes usar la palabra clave max
para especificar que tu rango de extensión vaya al número máximo de campos posible:
message Foo {
extensions 1000 to max;
}
max
es 229: 1 o 536,870,911.
Al igual que cuando eliges números de campo en general, la convención de numeración también debe evitar los campos de 19000 a 19999 (de FieldDescriptor::kFirstReservedNumber
a FieldDescriptor::kLastReservedNumber
), ya que están reservados para la implementación del búfer de protocolo. Puedes definir un rango de extensiones que incluya este rango, pero el compilador del protocolo no te permitirá definir extensiones reales con estos números.
Uno de
Si tienes un mensaje con muchos campos opcionales y en los que se configurará al menos un campo al mismo tiempo, puedes aplicar este comportamiento y ahorrar memoria con la función oneof.
Uno de los campos es como uno opcional, excepto todos los campos de una memoria compartida, y como máximo se puede configurar un campo al mismo tiempo. La configuración de cualquier miembro de uno de ellos borra automáticamente todos los demás miembros. Puedes verificar qué valor de un conjunto se configura (si existe) con un método especial case()
o WhichOneof()
, según el idioma elegido.
Usa Oneof
Para definir uno en tu .proto
, usa la palabra clave oneof
seguida de tu nombre, en este caso test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Luego, debe agregar sus campos oneof a la definición oneof. Puedes agregar campos de cualquier tipo, pero no puedes usar las palabras clave required
, optional
o repeated
. Si necesitas agregar un campo repetido a uno de ellos, puedes usar un mensaje que contenga el campo repetido.
En el código generado, uno de los campos tiene los mismos métodos get y set que los métodos optional
normales. También obtienes un método especial para verificar qué valor está configurado en uno de ellos. Puedes obtener más información sobre la API del idioma seleccionado en la referencia de la API relevante.
Una de las características
Si se configura un campo "oneof", se borrarán automáticamente todos los demás miembros de este. Por lo tanto, si configuras varios campos, solo el último campo que establezcas tendrá un valor.
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); message.mutable_sub_message(); // Will clear name field. CHECK(!message.has_name());
Si el analizador encuentra varios miembros del mismo cable en el cable, solo se usa el último que se vio en el mensaje analizado.
Las extensiones no son compatibles con uno de ellos.
Uno de ellos no puede ser
repeated
.Las API de reflexión funcionan para uno de los campos.
Si configuras un campo oneof con el valor predeterminado (como establecer un campo int32 oneof en 0), se establecerá el “caso” de ese campo one y el valor se serializará en el cable.
Si usas C++, asegúrate de que tu código no genere fallas en la memoria. El siguiente código de muestra fallará porque
sub_message
ya se borró mediante una llamada al métodoset_name()
.SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes here
Nuevamente en C++, si
Swap()
dos mensajes con uno de ellos, cada mensaje terminará con el caso del otro: en el siguiente ejemplo,msg1
tendrá unsub_message
ymsg2
tendrá unname
.SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
Problemas de retrocompatibilidad
Ten cuidado cuando agregues o quites uno de los campos. Si, cuando se verifica el valor de uno de estos, se muestra None
/NOT_SET
, podría significar que el oneof no se configuró o se configuró en un campo en una versión diferente del oneof. No hay forma de saber la diferencia, ya que no hay forma de saber si un campo desconocido en el cable es miembro de uno de ellos.
Problemas de reutilización de etiquetas
- Mover campos opcionales a uno de ellos o quitarlos de ellos: Es posible que pierdas parte de la información (algunos campos se borrarán) después de que el mensaje se serializa y analiza. Sin embargo, puedes mover de forma segura un solo campo a uno nuevo y quizás puedas mover varios campos si se sabe que solo se estableció uno. Consulta Actualiza un tipo de mensaje para obtener más detalles.
- Borra un campo One y vuelve a agregarlo: Es posible que se borre el campo oneof establecido actualmente después de que el mensaje se serializa y se analiza.
- Dividir o combinar uno: Esto tiene problemas similares a mover campos
optional
regulares.
Maps
Si deseas crear un mapa asociativo como parte de tu definición de datos, los búferes de protocolo proporcionan una sintaxis de acceso directo práctica:
map<key_type, value_type> map_field = N;
...en el que key_type
puede ser cualquier tipo integral o de string (así que cualquier tipo escalar, excepto los tipos de punto flotante y bytes
). Ten en cuenta que enum no es un key_type
válido. value_type
puede ser de cualquier tipo, excepto otro mapa.
Entonces, por ejemplo, si deseas crear un mapa de proyectos en el que cada mensaje Project
esté asociado con una clave de string, podrías definirlo de la siguiente manera:
map<string, Project> projects = 3;
Por el momento, la API de mapas generada se encuentra disponible para todos los idiomas compatibles con proto2. Puedes obtener más información sobre la API de Google Maps para el idioma elegido en la referencia de la API relevante.
Funciones de Maps
- Las extensiones no son compatibles con los mapas.
- Los mapas no pueden ser
repeated
,optional
nirequired
. - El orden de los cables y la iteración de mapas correspondientes a los valores de mapa no están definidos, por lo que no puedes confiar en que los elementos de tu mapa estén en un orden específico.
- Cuando se genera el formato de texto para una
.proto
, los mapas se ordenan por clave. Las claves numéricas se ordenan de forma numérica. - Durante el análisis desde la transferencia o la combinación, si se usan claves de mapa duplicadas, se utilizará la última clave que se vio. Cuando se analiza un mapa a partir de un formato de texto, el análisis puede fallar si hay claves duplicadas.
Compatibilidad con versiones anteriores
La sintaxis del mapa es equivalente a la siguiente en el cable, por lo que las implementaciones de búferes de protocolo que no admiten mapas aún pueden manejar tus datos:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Cualquier implementación de búfer de protocolo que admite mapas debe producir y aceptar datos que la definición anterior acepte.
Paquetes
Puedes agregar un especificador package
opcional a un archivo .proto
para evitar conflictos de nombres entre los tipos de mensajes de protocolo.
package foo.bar;
message Open { ... }
Luego, puedes usar el especificador de paquetes cuando definas campos de tu tipo de mensaje:
message Foo {
...
required foo.bar.Open open = 1;
...
}
La forma en que un especificador de paquetes afecta el código generado depende del lenguaje que hayas elegido:
- En C++, las clases generadas se unen en un espacio de nombres de C++. Por ejemplo,
Open
estaría en el espacio de nombresfoo::bar
. - En Java, el paquete se usa como el paquete de Java, a menos que proporciones explícitamente un
option java_package
en tu archivo.proto
. - En Python, se ignora la directiva
package
, ya que los módulos de Python se organizan según su ubicación en el sistema de archivos. - En Go, se ignora la directiva
package
y el archivo.pb.go
generado se encuentra en el paquete que lleva el nombre de la reglago_proto_library
correspondiente.
Ten en cuenta que, incluso cuando la directiva package
no afecta directamente el código generado, por ejemplo, en Python, se recomienda especificar el paquete del archivo .proto
, ya que, de lo contrario, podría generar conflictos en los descriptores y hacer que el protocolo no sea portátil para otros lenguajes.
Paquetes y resolución de nombres
La resolución de nombres de tipo en el lenguaje de búfer de protocolo funciona como C++: primero se busca el alcance más interno, luego el siguiente interno, y así sucesivamente, con cada paquete que se considera "interno" para su paquete superior. Un “.” inicial (por ejemplo, .foo.bar.Baz
) significa comenzar desde el alcance más externo.
El compilador del búfer de protocolo resuelve todos los nombres de tipos mediante el análisis de los archivos .proto
importados. El generador de códigos para cada idioma sabe cómo referirse a cada tipo en ese idioma, incluso si tiene diferentes reglas de alcance.
Definición de servicios
Si deseas usar tus tipos de mensajes con un sistema de RPC (llamada de procedimiento remoto), puedes definir una interfaz de servicio de RPC en un archivo .proto
, y el compilador del búfer de protocolo generará el código de la interfaz de servicio y los stubs en el lenguaje que hayas elegido. Entonces, por ejemplo, si deseas definir un servicio de RPC con un método que toma tu SearchRequest
y muestra un SearchResponse
, puedes definirlo en tu archivo .proto
de la siguiente manera:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
De forma predeterminada, el compilador del protocolo generará una interfaz abstracta llamada SearchService
y una implementación "stub" correspondiente. El stub reenvía todas las llamadas a un RpcChannel
, que, a su vez, es una interfaz abstracta que debes definir en términos de tu propio sistema de RPC. Por ejemplo, puedes implementar un RpcChannel
que serializa el mensaje y lo envía a un servidor a través de HTTP. En otras palabras, el stub generado proporciona una interfaz de tipo seguro para realizar llamadas RPC basadas en búferes de protocolo, sin bloquearte en ninguna implementación de RPC en particular. Por lo tanto, en C++, podrías obtener un código como el siguiente:
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;
}
Todas las clases de servicio también implementan la interfaz Service
, que proporciona una manera de llamar a métodos específicos sin conocer el nombre del método ni sus tipos de entrada y salida durante el tiempo de compilación. En el servidor, se puede usar para implementar un servidor RPC con el que puedas registrar servicios.
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;
}
Si no quieres conectar tu propio sistema de RPC existente, ahora puedes usar gRPC, un sistema de RPC de código abierto independiente de la plataforma y el lenguaje desarrollado en Google. gRPC funciona particularmente bien con búferes de protocolo y te permite generar el código RPC relevante directamente desde tus archivos .proto
con un complemento de compilador de búfer de protocolo especial. Sin embargo, como hay posibles problemas de compatibilidad entre clientes y servidores generados con proto2 y proto3, te recomendamos que uses proto3 para definir los servicios de gRPC. Puedes obtener más información sobre la sintaxis de proto3 en la guía del lenguaje Proto3. Si deseas usar proto2 con gRPC, debes usar la versión 3.0.0 o una versión posterior del compilador de búferes de protocolo y de las bibliotecas.
Además de gRPC, también hay una serie de proyectos de terceros en curso a fin de desarrollar implementaciones de RPC para los búferes de protocolo. Para obtener una lista de vínculos a proyectos que conocemos, consulta la página wiki de complementos de terceros.
Opciones
Las declaraciones individuales en un archivo .proto
se pueden anotar con una serie de opciones. Las opciones no cambian el significado general de una declaración, pero pueden afectar la forma en que se maneja en un contexto particular. La lista completa de las opciones disponibles se define en /google/protobuf/descriptor.proto
.
Algunas opciones son a nivel de archivo, lo que significa que se deben escribir en el nivel superior, no dentro de ningún mensaje, enumeración ni definición del servicio. Algunas opciones son opciones a nivel de mensaje, lo que significa que deben escribirse dentro de las definiciones de mensajes. Algunas son opciones a nivel de campo, lo que significa que deben escribirse dentro de las definiciones de campos. Las opciones también se pueden escribir en tipos de enumeración, valores de enumeración, uno de los campos, tipos de servicio y métodos de servicio. Sin embargo, por el momento no existen opciones útiles para ninguno de estos.
Estas son algunas de las opciones más usadas:
java_package
(opción de archivo): Es el paquete que deseas usar para tus clases de Java generadas. Si no se proporciona ninguna opción explícitajava_package
en el archivo.proto
, se usará el paquete de proto (especificado mediante la palabra clave "paquete" en el archivo.proto
) de forma predeterminada. Sin embargo, por lo general, los paquetes proto no son buenos paquetes de Java, ya que no se espera que los paquetes proto comiencen con nombres de dominio inversos. Si no se genera código Java, esta opción no tiene efecto.option java_package = "com.example.foo";
java_outer_classname
(opción de archivo): Es el nombre de clase (y, por lo tanto, el nombre de archivo) para la clase de Java de wrapper que deseas generar. Si no se especifica unjava_outer_classname
explícito en el archivo.proto
, el nombre de la clase se construirá convirtiendo el nombre del archivo.proto
en mayúsculas y minúsculas (de modo quefoo_bar.proto
se convierta enFooBar.java
). Si la opciónjava_multiple_files
está inhabilitada, todas las otras clases, enumeraciones, etc. generadas para el archivo.proto
se generarán dentro de esta clase de wrapper externo de Java como clases, enumeraciones, etc. anidadas. Si no se genera código Java, esta opción no tiene efecto.option java_outer_classname = "Ponycopter";
java_multiple_files
(opción de archivo): Si es falso, solo se generará un archivo.java
para este archivo.proto
, y todas las clases, enumeraciones, etc. de Java generadas para los mensajes de nivel superior, los servicios y las enumeraciones se anidarán dentro de una clase externa (consultajava_outer_classname
). Si es verdadero, se generarán archivos.java
separados para cada una de las clases, enumeraciones, clases, etc. generadas por los métodos, los archivos y los recursos Si no se genera código Java, esta opción no tiene efecto.option java_multiple_files = true;
optimize_for
(opción de archivo): se puede configurar enSPEED
,CODE_SIZE
oLITE_RUNTIME
. Esto afecta a los generadores de código C++ y Java (y, posiblemente, los de terceros) de las siguientes maneras:SPEED
(opción predeterminada): El compilador del búfer de protocolo generará código para serializar, analizar y realizar otras operaciones comunes en tus tipos de mensajes. Este código está altamente optimizado.CODE_SIZE
: El compilador del búfer de protocolo generará clases mínimas y se basará en el código compartido basado en reflexiones para implementar la serialización, el análisis y muchas otras operaciones. Por lo tanto, el código generado será mucho más pequeño que conSPEED
, pero las operaciones serán más lentas. Las clases seguirán implementando exactamente la misma API pública que en el modoSPEED
. Este modo es más útil en apps que contienen una gran cantidad de archivos.proto
y no necesitan que todas sean rápidas para los usuarios.LITE_RUNTIME
: El compilador del búfer de protocolo generará clases que dependen solo de la biblioteca del entorno de ejecución "lite" (libprotobuf-lite
en lugar delibprotobuf
). El entorno de ejecución lite es mucho más pequeño que la biblioteca completa (alrededor de un orden de magnitud más pequeño), pero omite ciertas funciones como los descriptores y la reflexión. Esto es particularmente útil para las apps que se ejecutan en plataformas limitadas, como los teléfonos celulares. El compilador seguirá generando implementaciones rápidas de todos los métodos, como lo hace en el modoSPEED
. Las clases generadas solo implementarán la interfazMessageLite
en cada lenguaje, lo que proporciona solo un subconjunto de los métodos de la interfazMessage
completa.
option optimize_for = CODE_SIZE;
cc_generic_services
,java_generic_services
,py_generic_services
(opciones de archivo): Indica si el compilador de búfer de protocolo debe generar código abstracto de servicio según las definiciones de los servicios en C++, Java y Python, respectivamente. Por motivos heredados, el valor predeterminado estrue
. Sin embargo, a partir de la versión 2.3.0 (enero de 2010), se considera preferible a las implementaciones de RPC proporcionar complementos de generación de códigos a fin de generar un código más específico para cada sistema, en lugar de depender de los servicios “abstractos”.// 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
(opción de archivo): Habilita la asignación de Arena para el código generado de C++.message_set_wire_format
(opción de mensaje): si se configura comotrue
, el mensaje usa un formato binario diferente destinado a ser compatible con un formato antiguo que se usa dentro de Google, llamadoMessageSet
. Es probable que los usuarios fuera de Google nunca necesiten usar esta opción. El mensaje debe declararse de la siguiente manera:message Foo { option message_set_wire_format = true; extensions 4 to max; }
packed
(opción de campo): Si se configura comotrue
en un campo repetido de un tipo numérico básico, se usa una codificación más compacta. Esta opción no tiene ningún inconveniente. Sin embargo, ten en cuenta que, antes de la versión 2.3.0, los analizadores que recibían datos empaquetados cuando no se esperaban los ignoraban. Por lo tanto, no era posible cambiar un campo existente al formato empaquetado sin romper la compatibilidad con los cables. En la versión 2.3.0 y posteriores, este cambio es seguro, ya que los analizadores de los campos empaquetables siempre aceptarán ambos formatos, pero ten cuidado si tienes que lidiar con los programas antiguos mediante versiones anteriores de protobuf.repeated int32 samples = 4 [packed = true];
deprecated
(opción de campo): si se establece entrue
, indica que el campo está obsoleto y no debe usarse en código nuevo. En la mayoría de los lenguajes, esto no tiene efecto real. En Java, esto se convierte en una anotación@Deprecated
. Para C++, clang-tidy generará advertencias cada vez que se usen campos obsoletos. En el futuro, es posible que otros generadores de código específicos de lenguaje generen anotaciones de baja en los descriptores de acceso del campo, lo que, a su vez, generará una advertencia cuando se compile el código que intenta usar el campo. Si nadie usa el campo y quieres evitar que lo usen nuevos usuarios, considera reemplazar la declaración de campo por una declaración reserved.optional int32 old_field = 6 [deprecated=true];
Opciones personalizadas
Los búferes de protocolo incluso te permiten definir y usar tus propias opciones. Ten en cuenta que esta es una función avanzada que la mayoría de las personas no necesitan. Dado que las opciones se definen mediante los mensajes definidos en google/protobuf/descriptor.proto
(como FileOptions
o FieldOptions
), definir tus propias opciones es solo una cuestión de extender esos mensajes. Por ejemplo:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
Aquí definimos una nueva opción a nivel de mensaje extendiendo MessageOptions
.
Cuando usamos la opción, el nombre de la opción debe encerrarse entre paréntesis para indicar que es una extensión. Ahora podemos leer el valor de my_option
en C++ de la siguiente manera:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
Aquí, MyMessage::descriptor()->options()
muestra el mensaje de protocolo MessageOptions
para MyMessage
. Leer las opciones personalizadas desde allí es como leer cualquier otra extensión.
De manera similar, en Java escribiríamos:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
.getExtension(MyProtoFile.myOption);
En Python sería así:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
Se pueden definir opciones personalizadas para cada tipo de construcción en el lenguaje de los búferes de protocolo. Este es un ejemplo en el que se usa todo tipo de opción:
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";
}
}
Ten en cuenta que, si quieres usar una opción personalizada en un paquete distinto del que se definió, debes anteponer el nombre de la opción al nombre del paquete, como lo harías con los nombres de los tipos. Por ejemplo:
// 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!";
}
Por último, dado que las opciones personalizadas son extensiones, se les deben asignar números de campo como cualquier otro campo o extensión. En los ejemplos anteriores, usamos números de campo en el rango 50000-99999. Este rango está reservado para uso interno dentro de organizaciones individuales, por lo que puedes usar números en este rango de forma gratuita para aplicaciones internas. Sin embargo, si quieres usar opciones personalizadas en aplicaciones públicas, es importante que te asegures de que los números de campo sean únicos a nivel global. A fin de obtener números de campo únicos a nivel global, envía una solicitud para agregar una entrada al registro de extensiones global de protobuf. Por lo general, solo se necesita un número de extensión. Puedes declarar varias opciones con un solo número de extensión. Para ello, debes colocarlas en un submensaje:
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" }];
}
Además, ten en cuenta que cada tipo de opción (a nivel de archivo, a nivel de mensaje, a nivel de campo, etc.) tiene su propio espacio numérico, por lo que, por ejemplo, puedes declarar extensiones de FieldOptions y MessageOptions con el mismo número.
Genera tus clases
Para generar el código Java, Python o C++ que necesitas para trabajar con los tipos de mensajes definidos en un archivo .proto
, debes ejecutar el compilador del búfer de protocolo protoc
en .proto
. Si no instalaste el compilador, descarga el paquete y sigue las instrucciones en el archivo README.
El compilador de protocolo se invoca de la siguiente manera:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
IMPORT_PATH
especifica un directorio en el que se deben buscar archivos.proto
cuando se resuelven directivasimport
. Si se omite, se usa el directorio actual. Para especificar varios directorios de importación, pasa la opción--proto_path
varias veces; se buscarán en orden.-I=_IMPORT_PATH_
se puede usar como una forma corta de--proto_path
.Puedes proporcionar una o más directivas de salida:
--cpp_out
genera código C++ enDST_DIR
. Consulta la referencia del código generado por C++ para obtener más información.--java_out
genera código Java enDST_DIR
. Consulta la referencia del código generado de Java para obtener más información.--python_out
genera código de Python enDST_DIR
. Consulta la referencia del código generado de Python para obtener más información.
Para tu comodidad, si el
DST_DIR
termina en.zip
o.jar
, el compilador escribirá el resultado en un solo archivo de formato ZIP con el nombre dado. Los resultados de.jar
también recibirán un archivo de manifiesto, según lo requiera la especificación Java JAR. Ten en cuenta que, si el archivo de salida ya existe, se reemplazará. El compilador no es lo suficientemente inteligente para agregar archivos a un archivo existente.Debes proporcionar uno o más archivos
.proto
como entrada. Se pueden especificar varios archivos.proto
a la vez. Si bien los archivos tienen un nombre relacionado con el directorio actual, cada uno debe residir en uno de losIMPORT_PATH
para que el compilador pueda determinar su nombre canónico.
Ubicación del archivo
Se prefiere no colocar los archivos .proto
en el mismo directorio que otras fuentes de lenguaje. Considera crear un subpaquete proto
para los archivos .proto
, en el paquete raíz de tu proyecto.
La ubicación no debe tener preferencias de idioma.
Cuando trabajas con código Java, es útil colocar los archivos .proto
relacionados en el mismo directorio que la fuente de Java. Sin embargo, si algún código que no es de Java usa los mismos protocolos, el prefijo de ruta de acceso ya no tendrá sentido. Por lo tanto, en general, coloca los protocolos en un directorio independiente del lenguaje relacionado, como //myteam/mypackage
.
La excepción a esta regla es cuando está claro que los protos se usan solo en un contexto de Java, por ejemplo, para pruebas.
Plataformas compatibles
Para obtener información sobre:
- consulta los sistemas operativos, los compiladores, los sistemas de compilación y las versiones de C++ que se admiten; consulta la Política de compatibilidad básica de C++.
- Las versiones de PHP compatibles, consulta Versiones de PHP compatibles.