Protobuf 協議語法與序列化原理實現


Protobuf 協議語法

  • message: Protobuf中定義一個數據結構需要用到關鍵字message,這一點和Java的class,Go語言中的struct類似。

  • 標識號: 在消息的定義中,每個字段等號后面都有唯一的標識號,用於在反序列化過程中識別各個字段的,一旦開始使用就不能改變。標識號從整數1開始,依次遞增,每次增加1,標識號的范圍為1~2^29 – 1,其中[19000-19999]為Protobuf協議預留字段,開發者不建議使用該范圍的標識號;一旦使用,在編譯時Protoc編譯器會報出警告。

  • 字段規則: 字段規則有三種:

    • 1、required:該規則規定,消息體中該字段的值是必須要設置的。
    • 2、optional:消息體中該規則的字段的值可以存在,也可以為空,optional的字段可以根據defalut設置默認值。
    • repeated:消息體中該規則字段可以存在多個(包括0個),該規則對應java的數組或者go語言的slice。
  • 數據類型: 常見的數據類型與protoc協議中的數據類型映射如下:

  •  img

  • 枚舉類型: proto協議支持使用枚舉類型,和正常的編程語言一樣,枚舉類型可以使用enum關鍵字定義在.proto文件中:

    enum Age{
        male=1;
        female=2;
    }
    

     

  • 字段默認值: .proto文件支持在進行message定義時設置字段的默認值,可以通過default進行設置,如下所示:

    message Address {
        required sint32 id = 1 [default = 1];
        required string name = 2 [default = '北京'];
        optional string pinyin = 3 [default = 'beijing'];
        required string address = 4;
        required bool flag = 5 [default = true];
    }

     

  • 導入: 如果需要引用的message是寫在別的.proto文件中,可以通過import "xxx.proto"來進行引入:

  • 嵌套: message與message之間可以嵌套定義,比如如下形式:

    syntax = "proto2";
    package example;
    message Person {
        required string Name = 1;
        required int32 Age = 2;
        required string From = 3;
        optional Address Addr = 4;
        message Address {
            required sint32 id = 1;
            required string name = 2;
            optional string pinyin = 3;
            required string address = 4;
        }
    }

     

  • message更新規則: message定義以后如果需要進行修改,為了保證之前的序列化和反序列化能夠兼容新的message,message的修改需要滿足以下規則:

    • 不可以修改已存在域中的標識號。
    • 所有新增添的域必須是 optional 或者 repeated。
    • 非required域可以被刪除。但是這些被刪除域的標識號不可以再次被使用。
    • 非required域可以被轉化,轉化時可能發生擴展或者截斷,此時標識號和名稱都是不變的。
    • sint32和sint64是相互兼容的。
    • fixed32兼容sfixed32。 fixed64兼容sfixed64。
    • optional兼容repeated。發送端發送repeated域,用戶使用optional域讀取,將會讀取repeated域的最后一個元素。

Protobuf 序列化后所生成的二進制消息非常緊湊,這得益於 Protobuf 采用的非常巧妙的 Encoding 方法。接下來看一看Protobuf協議是如何實現高效編碼的。

Protobuf序列化原理

之前已經做過描述,Protobuf的message中有很多字段,每個字段的格式為:修飾符 字段類型 字段名 = 域號;

Varint

Varint是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。

Varint中的每個byte的最高位bit有特殊的含義,如果該位為1,表示后續的byte也是該數字的一部分,如果該位為0,則結束。其他的7個bit都用來表示數字。因此小於128的數字都可以用一個byte表示。大於128的數字,比如300,會用兩個字節來表示:1010 1100 0000 0010。下圖演示了 Google Protocol Buffer 如何解析兩個bytes。注意到最終計算前將兩個byte的位置相互交換過一次,這是因為 Google Protocol Buffer 字節序采用little-endian的方式。

在序列化時,Protobuf按照TLV的格式序列化每一個字段,T即Tag,也叫Key;V是該字段對應的值value;L是Value的長度,如果一個字段是整形,這個L部分會省略。

序列化后的Value是按原樣保存到字符串或者文件中,Key按照一定的轉換條件保存起來,序列化后的結果就是 KeyValueKeyValue…依次類推的樣式,示意圖如下所示:

采用這種Key-Pair結構無需使用分隔符來分割不同的Field。對於可選的Field,如果消息中不存在該field,那么在最終的Message Buffer中就沒有該field,這些特性都有助於節約消息本身的大小。比如,我們有消息order1:

Order.id = 10;
Order.desc = "bill";

 

則最終的 Message Buffer 中有兩個Key-Value對,一個對應消息中的id;另一個對應desc。Key用來標識具體的field,在解包的時候,Protocol Buffer根據Key就可以知道相應的Value應該對應於消息中的哪一個field。

Key 的定義如下:

(field_number << 3) | wire_type

 

可以看到 Key 由兩部分組成。第一部分是 field_number,比如消息lm.helloworld中field id 的field_number為1。第二部分為wire_type。表示 Value的傳輸類型。而wire_type有以下幾種類型:

 

參考鏈接:golangroadmap

 


免責聲明!

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



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