十二.Protobuf3編碼


本文檔描述了協議緩沖消息的二進制格式。在應用程序中使用Protocol Buffer不需要理解這一點,但是了解不同的Protocol Buffer格式如何影響編碼消息的大小會非常有用。

一條簡單的信息

  假設您有以下非常簡單的消息定義:

message Test1 {
  optional int32 a = 1;
}

  在應用程序中,您創建一個Test1消息,並將設置為150。然后將消息序列化為輸出流。如果您能夠檢查編碼的消息,您會看到三個字節:

08 96 01

  到目前為止,數字如此之小——但這意味着什么呢?繼續閱讀...

Base 128 Varints

  要理解您的簡單Protocol Buffer編碼,您首先需要理解varints。Varints是一種使用一個或多個字節序列化整數的方法。較小的數字占用較小的字節數。

  變量中的每個字節,除了最后一個字節,都設置了最高有效位( msb ),這表明還會有更多的字節。每個字節的低7位用於存儲7位組中數字的二進制補碼表示,最低有效組優先。

  例如,這里是數字1——它是一個字節,所以msb沒有設置:

0000 0001

  這里是300,這有點復雜:

1010 1100 0000 0010

  你怎么知道這是300?首先從每個字節中刪除msb,因為這只是為了告訴我們是否已經到達數字的末尾(如您所見,它設置在第一個字節中,因為varint中有一個以上的字節) :

 1010 1100 0000 0010
→ 010 1100  000 0010

  顛倒兩組7位,因為正如您所記得的,變量首先存儲具有最低有效組的數字。然后將它們連接起來,得到最終值:

000 0010  010 1100
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

消息結構

  如您所知,協議緩沖消息是一系列鍵值對。消息的二進制版本僅使用字段的編號作為密鑰—每個字段的名稱和聲明類型只能在解碼端通過引用消息類型的定義(即.proto文件)來確定。

  當消息被編碼時,密鑰和值被連接成字節流。當消息被解碼時,解析器需要能夠跳過它不能識別的字段。這樣,新字段可以添加到郵件中,而不會破壞不知道它們的舊程序。為此,線格式消息中每一對的“鍵”實際上是兩個值—來自.proto文件的字段號,加上一個線類型,該線類型提供剛好足夠的信息來找到以下值的長度。在大多數語言實現中,這個鍵被稱為標簽。

  可用的消息類型如下:

類型 意義 用於
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit  fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (已廢棄)
4 End group groups (已廢棄)
5 32-bit fixed32, sfixed32, float

  流式消息中的每個密鑰都與值(字段編號< < 3) | wire_type )不同,換句話說,數字的最后三位存儲了wire類型。


  現在讓我們再看一遍我們的簡單例子。您現在知道了,數據流中的第一個數字總是一個不同的varint 鍵,這里是08,或者(去掉msb ) :

000 1000

  您取最后三位得到wire類型(0),然后右移三位得到字段號(1)。現在你知道字段號是1,下面的值是一個varint。使用上一節中不同的解碼知識,您可以看到接下來的兩個字節存儲了值150。

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (丟棄msb並反轉7位組)
       → 10010110
       → 128 + 16 + 4 + 2 = 150

更多值類型

有符號整數

  正如您在上一節中看到的,所有與類型0相關聯的Protocol Buffer類型都被編碼為varints。然而,在編碼負數時,有符號int類型( sint32和sint64 )和“標准”int類型( int32和int64 )之間有一個重要的區別。如果使用int32或int64作為負數的類型,得到的varints總是10字節長:實際上,它被視為一個非常大的無符號整數。如果您使用其中一種有符號類型,結果varints將使用ZigZag編碼,這種編碼效率更高。

  ZigZag編碼將有符號整數映射到無符號整數,因此絕對值小的數字(例如,-1)也有一個小的可變編碼值。它這樣做的方式是通過正整數和負整數來回“zig-zags”,因此-1編碼為1,1編碼為2,-2編碼為3,依此類推,如下表所示:

原始值 編碼為
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

 

  換句話說,每個值n使用:

(n << 1) ^ (n >> 31)

  對於 sint32, 

(n << 1) ^ (n >> 63)

  對於64位版本。

  請注意,第二個移位(n >> 31)部分是算術移位。換句話說,移位的結果要么是一個全為零位的數字(如果n為正),要么是全一位的數字(如果n為負)。

  當sint32或sint64被解析時,其值被解碼回原始的簽名版本。

非varint數字

  非varint數字類型很簡單:double和固定64位具有類型1,這告訴解析器期望固定的64位數據塊;類似地,float和固定32位具有類型5,這告訴它期望32位。在這兩種情況下,這些值都以小字節順序存儲。

字符串

  類型為2 (長度分隔)意味着該值是一個可變的編碼長度,后跟指定的數據字節數。

message Test2 {
  optional string b = 2;
}

  將b的值設置為“testing”:

12 07 74 65 73 74 69 6e 67

  紅色部分是“testing”的UTF8。這里的鍵是0x12 →字段號= 2,類型= 2。值的長度變化是7,我們發現后面有7個字節:我們的字符串。

嵌入消息

  下面是一個消息定義,其中嵌入了我們示例類型的消息,Test1 :

message Test3 {
  optional Test1 c = 3;
}

  這是編碼版本,Test1的字段設置為150 :

 1a 03 08 96 01

  如您所見,最后三個字節與我們的第一個示例( 08 96 01 )完全相同,前面是數字3:嵌入消息與字符串的處理方式完全相同(wire type = 2)。

可選和重復元素

  如果proto2消息定義有重復的元素(除了[packed=true]選項),則編碼的消息具有零個或多個具有相同字段編號的鍵值對。這些重復值不必連續出現;它們可以與其他字段交錯。在解析時,元素相對於彼此的順序會保留下來,盡管相對於其他字段的順序會丟失。在proto3中,重復字段使用打包編碼,您可以在下面閱讀。

  對於proto3中的任何非重復字段或proto2中的可選字段,編碼消息可能具有也可能不具有與該字段編號的鍵值對。

  通常,一個編碼的消息絕不會有一個以上的不重復字段實例。然而,解析器會處理這種情況。對於數字類型和字符串,如果同一個字段出現多次,解析器將接受它看到的最后一個值。對於嵌入的消息字段,解析器合並同一個字段的多個實例,就像使用Message::MergeFrom方法一樣:也就是說,后一個實例中的所有單個標量字段替換前一個實例中的字段,單個嵌入的消息被合並,重復的字段被連接。這些規則的效果是,解析兩個編碼消息的連接會產生完全相同的結果,就像您分別解析了這兩個消息並合並了結果對象一樣:

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

  相當於:

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

  這個屬性有時很有用,因為它允許您合並兩個消息,即使您不知道它們的類型。

打包重復字段

  2.1.0版引入了打包的重復字段,在proto2中,這些字段被聲明為類似於重復字段,但帶有特殊的[packed=true]選項。在proto3中,默認情況下,標量數值類型的重復字段被打包。這些功能像重復的字段,但編碼不同。編碼消息中不會出現包含零個元素的打包重復字段。否則,字段的所有元素都被打包成一個具有類型2 (長度分隔)的鍵值對。每個元素都以正常方式編碼,除了前面沒有鍵。

  例如,假設您有消息類型:

message Test4 {
  repeated int32 d = 4 [packed=true];
}

  現在假設您構建了一個Test4,為重復的字段d提供值3、270和86942。然后,編碼的形式將是:

22        // key (field number 4, wire type 2)
06        // payload size (6 bytes)
03        // first element (varint 3)
8E 02     // second element (varint 270)
9E A7 05  // third element (varint 86942)

  只有原始數字類型(使用變量、32位或64位類型的類型)的重復字段才能聲明為"packed"。

  請注意,雖然通常沒有理由為一個打包的重復字段編碼多個鍵值對,但編碼器必須准備好接受多個鍵值對。在這種情況下,有效載荷應該連接在一起。每對必須包含整數個元素。

  Protocol Buffer解析器必須能夠解析像未打包一樣打包的重復字段,反之亦然。這允許以向前和向后兼容的方式將[打包=真]添加到現有字段中。

字段排序

  字段編號可以在.proto 文件中以任何順序使用。選擇的順序對消息的序列化方式沒有影響。

  當消息被序列化時,對於如何寫入其已知或未知字段沒有保證順序。序列化順序是一個實現細節,任何特定實現的細節都可能在將來發生變化。因此,Protocol Buffer解析器必須能夠以任何順序解析字段。

含義

  不要假設序列化消息的字節輸出是穩定的。尤其是對於具有表示其他序列化Protocol Buffer消息的可傳遞字節字段的消息。

  默認情況下,對同一Protocol Buffer消息實例重復調用序列化方法可能不會返回相同的字節輸出;即默認串行化不是確定性的。

  --確定性序列化只保證特定二進制文件的相同字節輸出。字節輸出可能在二進制文件的不同版本之間發生變化。

  對於Protocol Buffer消息實例foo,以下檢查可能會失敗。

 foo.SerializeAsString() == foo.SerializeAsString()
  Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
  CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
  FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())

  這里有幾個示例場景,其中邏輯等價的協議緩沖消息foo和bar可以序列化為不同的字節輸出。

  -bar由舊服務器序列化,舊服務器將某些字段視為未知字段。

  -bar由用不同編程語言實現的服務器序列化,並以不同的順序序列化字段。

  -bar有一個以非確定性方式序列化的字段。

  -bar有一個存儲Protocol Buffer消息序列化字節輸出的字段,該消息以不同方式序列化。

  -bar由新的服務器序列化,由於實現的變化,該服務器以不同的順序序列化字段。

  -foo和bar都是單個消息的串聯,但順序不同。

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM