OBSERVAÇÃO:este site foi descontinuado. O site será desativado após 31 de janeiro de 2023, e o tráfego será redirecionado para o novo site em https://protobuf.dev. Enquanto isso, as atualizações serão feitas apenas para protobuf.dev.

Codificação

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Este documento descreve o formato de transmissão do buffer de protocolo, que define os detalhes de como sua mensagem é enviada em trânsito e quanto espaço ela consome no disco. Você provavelmente não precisa entender isso para usar buffers de protocolo no seu aplicativo, mas são informações úteis para fazer otimizações.

Se você já conhece os conceitos, mas quer uma referência, pule para a seção Card de referência condensado.

O protoscópio é uma linguagem muito simples para descrever snippets do formato de transmissão de baixo nível, que usaremos para fornecer uma referência visual para a codificação de várias mensagens. A sintaxe do protoscope consiste em uma sequência de tokens que cada um codifica para uma sequência de bytes específica.

Por exemplo, crases indicam um literal hexadecimal bruto, como `70726f746f6275660a`. Isso é codificado nos bytes exatos marcados como hexadecimais no literal. As aspas denotam strings UTF-8, como "Hello, Protobuf!". Esse literal é sinônimo de `48656c6c6f2c2050726f746f62756621`, que, se você observar de perto, é composto por bytes ASCII. Vamos falar mais sobre a linguagem de protótipo ao discutir aspectos do formato de transmissão.

A ferramenta Protoscope também pode despejar buffers de protocolo codificados como texto. Consulte testdata para ver exemplos.

Uma mensagem simples

Digamos que você tenha a seguinte definição de mensagem muito simples:

message Test1 {
  optional int32 a = 1;
}

Em um aplicativo, crie uma mensagem Test1 e defina a como 150. Em seguida, serializa a mensagem para um stream de saída. Se você pudesse examinar a mensagem codificada, veria três bytes:

08 96 01

Até aqui, tão pequeno e numérico, mas o que isso significa? Se você usar a ferramenta Protoscope para despejar esses bytes, vai receber algo como 1: 150. Como ele sabe que esse é o conteúdo da mensagem?

128 variedades de base

Números inteiros de largura variável, ou varints, são a base do formato de transmissão. Eles permitem codificar números inteiros de 64 bits não assinados usando qualquer lugar entre um e dez bytes, com valores pequenos usando menos bytes.

Cada byte na varint tem um bit de continuação que indica se o byte que a segue faz parte da varint. Esse é o bit mais significativo (MSB, na sigla em inglês) do byte (às vezes, também chamado de bit bit de sinal). Os 7 bits mais baixos são um payload. O número inteiro resultante é anexado aos payloads de 7 bits dos bytes constituintes dele.

Por exemplo, este é o número 1, codificado como `01`. Ele é um único byte, portanto, o MSB não está definido:

0000 0001
^ msb

E aqui está 150, codificado como `9601`. Isso é um pouco mais complicado:

10010110 00000001
^ msb    ^ msb

Como você descobre que é 150? Primeiro, elimine o MSB de cada byte, já que ele serve apenas para nos informar se chegamos ao fim do número (como você pode ver, ele é definido no primeiro byte, já que há mais de um byte no varint). Em seguida, concatenamos os payloads de 7 bits e o interpretamos como um número inteiro não assinado de 64 bits:

10010110 00000001        // Original inputs.
 0010110  0000001        // Drop continuation bits.
 0000001  0010110        // Put into little-endian order.
 10010110                // Concatenate.
 128 + 16 + 4 + 2 = 150  // Interpret as integer.

Como os variedades são cruciais para os buffers de protocolo, nos referimos a eles como números inteiros. 150 é igual a `9601`.

Estrutura da mensagem

Uma mensagem do buffer de protocolo é uma série de pares de chave-valor. A versão binária de uma mensagem usa apenas o número do campo como chave. O nome e o tipo declarado para cada campo só podem ser determinados no final da decodificação ao referenciar a definição do tipo de mensagem (ou seja, o arquivo .proto). O protótipo não tem acesso a essas informações, portanto, ele só pode fornecer os números de campo.

Quando uma mensagem é codificada, cada par de chave-valor é transformado em um registro com o número do campo, um tipo de transferência e um payload. O tipo de transmissão informa ao analisador quanto o payload é necessário. Isso permite que os analisadores antigos ignorem novos campos que não entendem. Esse tipo de esquema às vezes é chamado de Tag-Length-Value, ou TLV.

Há seis tipos de fios: VARINT, I64, LEN, SGROUP, EGROUP e I32

ID Nome Usado para
0 VÁRIAS int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 I64. fixar64, fixo64, duplo
2 LEN string, bytes, mensagens incorporadas, campos repetidos compactados
3 GRUPO início do grupo (descontinuado)
4 EGROUP fim do grupo (descontinuado)
5 P32 fixed32, sFixed32, flutuante

A "tag" de um registro é codificada como um varint formado pelo número do campo e pelo tipo de fio pela fórmula (field_number << 3) | wire_type. Em outras palavras, após a decodificação do varint que representa um campo, os três bits mais baixos informam o tipo de fio, e o restante do número inteiro informa o número do campo.

Agora vamos ver novamente nosso exemplo simples. Agora, você sabe que o primeiro número no stream é sempre uma chave varint e, aqui, é `08`, ou (descartando o MSB):

000 1000

Use os três últimos bits para chegar ao tipo de transmissão (0) e mude a posição em três para a direita para descobrir o número do campo (1). O protótipo representa uma tag como um número inteiro seguido de dois-pontos e o tipo de fio. Assim, podemos gravar os bytes acima como 1:VARINT.

Como o tipo de transmissão é 0, ou VARINT, sabemos que precisamos decodificar um varint para receber o payload. Como vimos acima, os bytes `9601` varint-decode para 150, proporcionando nosso registro. Podemos gravá-lo no protótipo como 1:VARINT 150.

O protótipo poderá inferir o tipo de uma tag se houver espaços em branco depois do :. Isso é feito considerando o próximo token e adivinhando o que você quer dizer. As regras estão documentadas em detalhes no language.txt do Protoscope. Por exemplo, em 1: 150, há uma variedade imediatamente após a tag sem tipo. Portanto, o protoscópio infere o tipo como VARINT. Se você tivesse escrito 2: {}, ele veria o { e adivinharia LEN. Se você tivesse escrito 3: 5i32, ele adivinharia I32 e assim por diante.

Mais tipos de números inteiros

Bool e Enums

Booleanos e enumerações são codificados como se fossem int32s. Os booleanos, em particular, sempre codificam como `00` ou `01`. No Protoscope, false e true são aliases dessas strings de bytes.

Números inteiros assinados

Como você viu na seção anterior, todos os tipos de buffer de protocolo associados ao tipo de fio 0 são codificados como varints. No entanto, os variedades não têm assinatura, então os diferentes tipos assinados, sint32 e sint64, em comparação com int32 ou int64, codificam números inteiros negativos de maneiras diferentes.

Os tipos intN codificam números negativos como complemento de dois, o que significa que, como números inteiros de 64 bits não assinados, eles têm o maior conjunto de bits. Como resultado, isso significa que todos os 10 bytes precisam ser usados. Por exemplo, -2 é convertido por protoscope em

11111110 11111111 11111111 11111111 11111111
11111111 11111111 11111111 11111111 00000001

Esse é o complemento de dois de 2, definido em aritmética não assinada como ~0 - 2 + 1, em que ~0 é o número inteiro de 64 bits. É um exercício útil para entender por que isso produz tantos.

Por outro lado, sintN usa a codificação "ZigZag" em vez do complemento de dois para codificar números inteiros negativos. Os números inteiros positivos n são codificados como 2 * n (os números pares), enquanto os números inteiros negativos -n são codificados como 2 * n + 1 (os números ímpares). Assim, a codificação "zig-zags" entre números positivos e negativos. Exemplo:

Original assinado Codificado como
0 0
-1 1
1 2
-2 3
0x7fffffff 0xfffffffe
-0x80000000 0xffffffff

Usando alguns truques, é barato converter n em sua representação ZigZag:

n + n + (n < 0)

Aqui, presumimos que o booleano n < 0 é convertido em um número inteiro 1 se for verdadeiro ou um número inteiro 0 se for falso.

Quando o sint32 ou sint64 é analisado, o valor dele é decodificado de volta para a versão original assinada.

No protótipo, a adição de um número inteiro com um z como sufixo fará com que ele seja codificado como ZigZag. Por exemplo, -500z é igual à variável 1001.

Números não variados

Os tipos numéricos não diversos são simples: double e fixed64 têm o tipo de fio I64, que informa ao analisador que é esperado um volume fixo de dados de oito bytes. É possível especificar um registro double gravando 5: 25.4 ou um registro fixed64 com 6: 200i64. Em ambos os casos, a omissão de um tipo de fiação explícito infere o tipo de fiação I64.

Da mesma forma, float e fixed32 têm o tipo de transmissão I32, que informa que é preciso esperar quatro bytes no lugar. A sintaxe deles consiste na adição de um prefixo i32. A 25.4i32 emite quatro bytes, assim como a 200i32. Os tipos de tags são inferidos como I32.

Registros com duração ilimitada

Os prefixos de comprimento são outro conceito importante no formato de transmissão. O tipo de fiação LEN tem um comprimento dinâmico, especificado por uma variedade imediatamente após a tag, que é seguida pelo payload normalmente.

Considere este esquema de mensagens:

message Test2 {
  optional string b = 2;
}

Um registro para o campo b é uma string que tem codificação LEN. Se definirmos b como "testing", codificamos como um registro LEN com o campo número 2 contendo a string ASCII "testing". O resultado é `120774657374696e67`. Dividindo os bytes,

12 07 [74 65 73 74 69 6e 67]

vemos que a tag `12` é 00010 010 ou 2:LEN. O byte seguinte é a variedade 7 e os próximos sete bytes são a codificação UTF-8 de "testing".

No Protoscope, isso é escrito como 2:LEN 7 "testing". No entanto, pode ser incorreto repetir o comprimento da string (que, no texto do protótipo, já está delimitado por aspas). Unir o conteúdo do protótipo entre chaves vai gerar um prefixo de comprimento: {"testing"} é uma abreviação de 7 "testing". O {} é sempre inferido por campos para ser um registro LEN. Portanto, podemos gravar esse registro simplesmente como 2: {"testing"}.

Os campos bytes são codificados da mesma maneira.

Submensagens

Os campos de submensagem também usam o tipo de transferência LEN. Veja uma definição de mensagem com uma mensagem incorporada da mensagem de exemplo original, Test1:

message Test3 {
  optional Test1 c = 3;
}

Se o campo a de Test1 (por exemplo, c.a do Test3) estiver definido como 150, o resultado será 1a03089601. Divisão:

 1a 03 [08 96 01]

Os últimos três bytes (em []) são exatamente os mesmos do nosso primeiro exemplo. Esses bytes são precedidos por uma tag do tipo LEN e um comprimento de três, exatamente da mesma forma que as strings são codificadas.

No Protoscope, as submensagens são bem sucintas. 1a03089601 pode ser escrito como 3: {1: 150}.

Elementos opcionais e repetidos

Os campos optional ausentes são fáceis de codificar: basta não incluir o registro se ele não estiver presente. Isso significa que os protos "enormes" com apenas alguns campos definidos são esparsos.

Os campos repeated são um pouco mais complicados. Os campos repetidos (não empacotados) emitem um registro para cada elemento do campo. Assim, se tivermos

message Test4 {
  optional string d = 4;
  repeated int32 e = 5;
}

e construímos uma mensagem Test4 com d definido como "hello" e e definido como 1, 2 e 3, que pode ser codificado como `220568656c6c6f280128022803` ou escrito como Protoscope,

4: {"hello"}
5: 1
5: 2
5: 3

No entanto, os registros de e não precisam aparecer consecutivamente e podem ser intercalados com outros campos. Apenas a ordem dos registros para o mesmo campo em relação a eles é preservada. Portanto, isso também poderia ter sido codificado como

5: 1
5: 2
4: {"hello"}
5: 3

Não há tratamento especial para oneofs no formato de transmissão.

O último vence

Normalmente, uma mensagem codificada nunca teria mais de uma instância de um campo que não seja repeated. No entanto, os analisadores precisam processar o caso em que fazem isso. Para tipos e strings numéricos, se o mesmo campo aparecer várias vezes, o analisador aceitará o último valor mostrado. Para campos de mensagens incorporados, o analisador mescla várias instâncias do mesmo campo, como se você estivesse usando o método Message::MergeFrom. Ou seja, todos os campos escalares singulares da última instância substituem os da anterior, as mensagens incorporadas singulares são mescladas e os campos repeated são concatenados. O efeito dessas regras é que analisar a concatenação de duas mensagens codificadas produz exatamente o mesmo resultado que se você tivesse analisado as duas mensagens separadamente e mesclado os objetos resultantes. Ou seja:

MyMessage message;
message.ParseFromString(str1 + str2);

é equivalente a:

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

Essa propriedade é útil às vezes porque permite mesclar duas mensagens (por concatenação), mesmo que você não saiba os tipos delas.

Campos repetidos em pacotes

A partir da versão 2.1.0, os campos repeated do tipo escalar podem ser declarados como "empacotados". No proto2, isso é feito com o [packed=true], mas no proto3, é o padrão.

Em vez de serem codificados como um registro por entrada, eles são codificados como um único registro LEN que contém cada elemento concatenado. Para decodificar, os elementos são decodificados do registro LEN um por um até o payload ser esgotado. O início do próximo elemento é determinado pelo comprimento do anterior, que depende do tipo do campo.

Por exemplo, imagine que você tem o tipo de mensagem:

message Test5 {
  repeated int32 f = 6 [packed=true];
}

Agora, digamos que você construa um Test5, fornecendo os valores 3, 270 e 86942 para o campo repetido f. Codificado, isso nos dá `3206038e029ea705`, ou texto de protótipo,

6: {3 270 86942}

Somente campos repetidos de tipos numéricos primitivos podem ser declarados como "empacotados". Esses são os tipos que normalmente usam os tipos de fio VARINT, I32 ou I64.

Embora não haja motivo para codificar mais de um par de chave-valor em um campo repetido, os analisadores precisam estar preparados para aceitar vários pares de chave-valor. Nesse caso, os payloads precisam ser concatenados. Cada par precisa conter um número inteiro de elementos. Veja a seguir uma codificação válida da mesma mensagem acima que os analisadores precisam aceitar:

6: {3 270}
6: {86942}

Os analisadores de buffer de protocolo precisam ser capazes de analisar campos repetidos que foram compilados como packed como se não fossem compactados e vice-versa. Isso permite adicionar [packed=true] a campos existentes de maneira compatível com versões futuras e versões anteriores.

Mapas

Os campos do mapa são apenas uma forma abreviada de um tipo especial de campo repetido. Se

message Test6 {
  map<string, int32> g = 7;
}

este é o mesmo que

message Test6 {
  message g_Entry {
    optional string key = 1;
    optional int32 value = 2;
  }
  repeated g_Entry g = 7;
}

Assim, os mapas são codificados exatamente como um campo de mensagem repeated: como uma sequência de registros do tipo LEN, com dois campos cada.

Grupos

Os grupos são um recurso descontinuado que não deve ser usado, mas permanecem no formato de transmissão e merecem ser mencionados.

Um grupo é um pouco como uma submensagem, mas é delimitado por tags especiais, em vez de um prefixo LEN. Cada grupo em uma mensagem tem um número de campo, que é usado nessas tags especiais.

Um grupo com o campo de número 8 começa com uma tag 8:SGROUP. Os registros SGROUP têm payloads vazios, então isso indica o início do grupo. Depois que todos os campos do grupo forem listados, uma tag 8:EGROUP correspondente vai indicar o fim. Como os registros EGROUP também não têm payload, o 8:EGROUP é todo o registro. Os números de campo do grupo precisam ser correspondentes. Se encontrarmos 7:EGROUP onde esperamos 8:EGROUP, a mensagem estará mal formada.

O protótipo oferece uma sintaxe conveniente para criar grupos. Em vez de escrever

8:SGROUP
  1: 2
  3: {"foo"}
8:EGROUP

O protótipo permite

8: !{
  1: 2
  3: {"foo"}
}

Isso vai gerar os marcadores de grupo inicial e final adequados. A sintaxe !{} só pode ocorrer imediatamente após uma expressão de tag sem tipo, como 8:.

Ordem do campo

Os números de campo podem ser declarados em qualquer ordem em um arquivo .proto. A ordem escolhida não afeta a maneira como as mensagens são serializadas.

Quando uma mensagem é serializada, não há uma garantia de como os campos desconhecidos ou conhecidos serão gravados. A ordem de serialização é um detalhe da implementação, e os detalhes de qualquer implementação específica podem mudar no futuro. Portanto, os analisadores de buffer de protocolo precisam ser capazes de analisar campos em qualquer ordem.

Implicações

  • Não suponha que a saída de bytes de uma mensagem serializada seja estável. Isso é especialmente verdadeiro para mensagens com campos de bytes transitivos que representam outras mensagens de buffer de protocolo serializadas.
  • Por padrão, invocações repetidas de métodos de serialização na mesma instância de mensagem de buffer de protocolo podem não produzir a mesma saída de byte. Ou seja, a serialização padrão não é determinista.
    • A serialização determinística garante apenas a mesma saída de byte em um binário específico. A saída de bytes pode mudar em diferentes versões do binário.
  • As seguintes verificações podem falhar para uma instância da mensagem de buffer de protocolo foo:
    • foo.SerializeAsString() == foo.SerializeAsString()
    • Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
    • CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
    • FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
  • Veja alguns exemplos de situações em que as mensagens de buffer de protocolo logicamente equivalentes foo e bar podem ser serializadas em diferentes saídas de bytes:
    • bar é serializado por um servidor antigo que trata alguns campos como desconhecidos.
    • bar é serializado por um servidor que é implementado em uma linguagem de programação diferente e serializa campos em ordem diferente.
    • bar tem um campo que é serializado de maneira não determinista.
    • bar tem um campo que armazena uma saída de bytes serializada de uma mensagem de buffer de protocolo, que é serializada de maneira diferente.
    • bar é serializado por um novo servidor que serializa campos em uma ordem diferente devido a uma mudança de implementação.
    • foo e bar são concatenações das mesmas mensagens individuais em uma ordem diferente.

Card de referência condensado

Veja a seguir as partes mais proeminentes do formato de transmissão em um formato fácil de referência.

message    := (tag value)*

tag        := (field << 3) bit-or wire_type;
                encoded as varint
value      := varint      for wire_type == VARINT,
              i32         for wire_type == I32,
              i64         for wire_type == I64,
              len-prefix  for wire_type == LEN,
              <empty>     for wire_type == SGROUP or EGROUP

varint     := int32 | int64 | uint32 | uint64 | bool | enum | sint32 | sint64;
                encoded as varints (sintN are ZigZag-encoded first)
i32        := sfixed32 | fixed32 | float;
                encoded as 4-byte little-endian;
                memcpy of the equivalent C types (u?int32_t, float)
i64        := sfixed64 | fixed64 | double;
                encoded as 8-byte little-endian;
                memcpy of the equivalent C types (u?int32_t, float)

len-prefix := size (message | string | bytes | packed);
                size encoded as varint
string     := valid UTF-8 string (e.g. ASCII);
                max 2GB of bytes
bytes      := any sequence of 8-bit bytes;
                max 2GB of bytes
packed     := varint* | i32* | i64*,
                consecutive values of the type specified in `.proto`

Consulte também a Referência da linguagem do protótipo.

Chave

message := (tag value)*
Uma mensagem é codificada como uma sequência de zero ou mais pares de tags e valores.
tag := (field << 3) bit-or wire_type
Uma tag é uma combinação de um wire_type, armazenado nos três bits menos significativos, e no número de campo definido no arquivo .proto.
value := varint for wire_type == VARINT, ...
Um valor é armazenado de forma diferente dependendo do wire_type especificado na tag.
varint := int32 | int64 | uint32 | uint64 | bool | enum | sint32 | sint64
É possível usar o varint para armazenar qualquer um dos tipos de dados listados.
i32 := sfixed32 | fixed32 | float
É possível usar fixa32 para armazenar qualquer um dos tipos de dados listados.
i64 := sfixed64 | fixed64 | double
É possível usar fixa64 para armazenar qualquer um dos tipos de dados listados.
len-prefix := size (message | string | bytes | packed)
Um valor com prefixo de tamanho é armazenado como um comprimento (codificado como um varint) e um dos tipos de dados listados.
string := valid UTF-8 string (e.g. ASCII)
Como descrito, uma string deve usar a codificação de caracteres UTF-8. Uma string não pode exceder 2 GB.
bytes := any sequence of 8-bit bytes
Como descrito, os bytes podem armazenar tipos de dados personalizados de até 2 GB.
packed := varint* | i32* | i64*
Use o tipo de dados packed ao armazenar valores consecutivos do tipo descrito na definição do protocolo. A tag é descartada para valores após o primeiro, o que amortiza os custos de tags em uma por campo, em vez de por elemento.