1.了解Protobuf
Protocol Buffer是Google的語言中立的,平台中立的,可擴展機制的,用於序列化結構化數據 - 對比XML,但更小,更快,更簡單。您可以定義數據的結構化,然后可以使用特殊生成的源代碼輕松地在各種數據流中使用各種語言編寫和讀取結構化數據。
簡單的來說,ProtoBuf和json、xml一樣是一種結構化的數據格式,用於數據通信的傳輸及數據的存儲。但ProtoBuf相比json和xml來說具有以下的優點:
- 性能好,效率高:是一種二進制的數據格式,比xml小3-5倍,其速度是xml的20-100倍。
- 代碼生成機制,數據解析類自動生成:提供了根據proto文件生成對應的源文件代碼生成機制。windows(proto.exe)、linux平台動態編譯生成
- 支持向后和向前兼容:兼容以前和以后的其他版本,更新數據結構,不影響破壞原有的舊程序。
- 支持多種編譯語言:提供了C++、python、java多種語言的支持。
缺點:
- 其內部格式是二進制,導致數據可讀性差。
1.1.Protobuf的安裝
- 在官網找到適合系統的版本,下載,並解壓,我的虛擬機時CentOS7,下載的是protoc-3.20.0-linux-x86_64.zip,解壓: unzip protoc-3.20.0-linux-x86_64.zip
- 進入解壓后的目錄,把./bin/protoc放在/usr/local/bin可執行程序目錄中,這樣全局都可以訪問到,同時把include目錄的內容也復制到/usr/local/include/中
- 驗證安裝:protoc –version (正常有版本信息返回)
1.2.Protobuf的語法
要使用Protobuf,我們首先需要定義我們要傳輸的消息。消息在.proto
文件內定義。這里先看個.proto的樣例(addressbook.proto),之后以此消息格式做應用。
1 syntax = "proto3"; // 指定protobuf語法版本 2 package myProto; /* 指定pkg的包名 */ 3 4 message Person { 5 string name = 1; 6 int32 id = 2; 7 string email = 3; 8 9 enum PhoneType { 10 MOBILE = 0; 11 HOME = 1; 12 WORK = 2; 13 } 14 15 message PhoneNumber { 16 string number = 1; 17 PhoneType type = 0; 18 } 19 20 repeated PhoneNumber phone = 4; 21 22 map<int32, int32> mapfield = 5; 23 } 24 25 message AddressBook { 26 repeated Person person = 1; 27 }
- syntax:語法定義了規范使用哪個版本的Protobuf。這里指定
proto3,如果您不這樣做,protobuf 編譯器將假定您正在使用proto2。這必須是文件的第一個非空的非注釋行。
- import:如果根據另一條消息定義了一條消息,則需要使用
import
語句將其包括在內。 - package:包定義了屬於同一名稱空間的消息。這樣可以防止名稱沖突,注意名稱需要唯一(如果有多個.proto文件)
- messsage:消息是我們想使用Protobuf建模的一條信息(field消息字段的集合)。
- 注釋:proto的注釋和C一樣,有兩種方式: // 或 /* 。。。*/
protobuf2中.proto文件中的數據結構由以下幾部分組成:
- 關鍵字message:代表實體結構,由多個消息字段(field)組成。
- 消息字段: 由數據類型、字段名、字段規則、字段唯一標識、默認值組成。
- 數據類型:
- 復合型數據類型:枚舉、map、message類型
- 標准數據類型:整型、浮點、字符串等
- 字段規則:
- required:必須初始化字段,如果沒有賦值,在數據序列化時會拋出異常
- optional:可選字段,可以不賦值。如果沒有賦值,會使用默認值
- repeated:表示該字段可以重復任意次數,包括0次。重復數據的順序將會保存在protocol buffer中。
- 字段唯一標識:每個字段都有唯一的數字標識符。用於標記該字段在序列化后的二進制數據中輸在的field,每個字段的唯一數字標識符在message內部都是獨一無二的。
- 默認值:在定義消息字段時可以給出默認值
Potobuf3與Protobuf2不同的地方:
1、字段規則:
- 字段前取消了required和optional兩個關鍵字,目前只保留了repeated關鍵字。
- 修飾消息的字段修飾符必須是singular、或repeated。
- singular:一個格式良好的消息應該有0個或者1個這種字段(但是不能超過1個)。
- repeated:在一個格式良好的消息中,這種字段可以重復任意多次(包括0次)。重復的值的順序會被保留。
- map數據類型前面不能加repeated修飾符
2、取消了設置默認值:
- string默認為字符串
- bytes默認為空bytes
- bool默認為false
- 數字類型默認為0
- 枚舉類型默認為第一個枚舉定義的第一個值。且第一個值必須為0。(注意:定義為枚舉類型的數據,如果值對應的是枚舉的第一個(0),則在顯示時,默認不顯示)
3、支持的數據類型有:
double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes
4、分配標識符:
正如上述文件格式,在消息定義中,每個字段都有唯一的一個數字標識符。這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不能夠再改變。
注意:[1,15]之內的標識號在編碼的時候會占用一個字節。[16,2047]之內的標識號則占用2個字節。所以應該為那些頻繁出現的消息元素保留 [1,15]之內的標識號。切記:要為將來有可能添加的、頻繁出現的標識號預留一些標識號。
最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto文件中使用這些預留標識號,編譯時就會報錯。
1.3.Protoc的編譯
要使用protobuf的消息協議,則必須把.proto文件編譯生成對應語言的文件,我這里生成的是python,在同.proto文件目錄下,會生成addressbook_pb2.py,這里是以syntax = "proto3"方式生成,如下圖一。(用proto2生成的python代碼文件有點不一樣)
注意:不要修改此文件
語法:具體根據 protoc --help 查看,或參考官網,下面是簡單的命令語法,后面會介紹自動化的Protobuf編譯
- protoc addressbook.proto --cpp_out=. # 生成c
- protoc addressbook.proto --python_out=. # 生成python
- protoc addressbook.proto --go_out=. # 生成go,需要另外的程序protoc-gen-go
2.Python的應用
2.1.protobuf模塊安裝
在正式開發前需要先安裝proto的python模塊,如下2個命令,在安裝好后,我們還需要下載protobuf-all-3.20.0.tar.gz文件(當然也可以用這個包安裝protobuf模塊),把壓縮包中的 protobuf-all-3.20.0.tar.gz/protobuf-3.20.0/python/google/protobuf/internal/builder.py文件復制到python的protobuf模塊的相應位置下:./lib/site-packages/google/protobuf/internal/builder.py,因為以proto3生成的python代碼,有個builder的模塊需要引入,而通過pip安裝的protobuf模塊,缺少這個builder.py文件。
- pip install google
- pip install protobuf
有了上面protobuf編譯生成的python代碼文件,以及protobuf模塊,我們就可以用應用了。
2.2.用ptyhon填充protobuf對象
在序列化protobuf對象前,需要用數據來填充它,這里以上面的.protoc文件為例填充。填充的方式有2中:
- 以定義protobuf的.protoc文件的格式,一步步把數據填充上,這種方式適合protoc的消息格式不怎么變的情況,因為以這種方式,protocbuf對象的初始化是嚴格按照.protoc定義的,如果.protoc的格式變化,就需要修改相應程序邏輯;(網絡截圖)
- 還有一種是通過根據protoc的消息格式編排好的json/dict對象,結合protobuf對象的屬性方法,把數據填充上去,這種方法靈活通用,protoc消息格式的改變,只要改變json/dict的數據格式,不需要修改程序代碼,就能實現protoc對象的數據填充;
2.3.用Python序列化ProtoBuf對象
把數據填充到protobuf對象后,就可以通過調用SerializeToString()函數來序列化,ParseFromString()函數來反序列化。序列化后以二進制的形式呈現,對於反序列化,類似和protobuf的數據填充一樣有2中方式。
注意:通過SerializeToString()函數序列化,返回的是序列化后的二進制數據,而通過ParseFromString()函數反序列化,返回的是序列化后的二進制數據的長度,並不是實際的數據,實際的數據是protobuf對象(代碼中的obj對象);
2.4.測試樣例
下圖中,第一個引入的模塊名,為protoc編譯生成的python文件的模塊名,后面的類名AddressBook為.proto文件中定義的message名稱;
第二個引入的是對protobuf對象的操作模塊類;
上面執行的結果:
第一個print是dict數據轉換填充到protobuf對象,第二個print是把protobuf對象中的數據轉換成json數據格式,第三個print是序列化,第四個print是反序列化及序列化后的長度;
1 2 Dict->Protobuf::person { 3 name: "John" 4 id: 1 5 phone { 6 number: "+1234567890" 7 type: WORK 8 maps { 9 mapfield { 10 key: 1 11 value: 11 12 } 13 mapfield { 14 key: 6 15 value: 66 16 } 17 } 18 } 19 phone { 20 number: "+2345678901" 21 type: HOME 22 } 23 } 24 person { 25 name: "Ben Bun" 26 id: 2 27 email: "b@bun.com" 28 phone { 29 number: "+1234567890" 30 maps { 31 mapfield { 32 key: 5 33 value: 55 34 } 35 mapfield { 36 key: 6 37 value: 66 38 } 39 } 40 } 41 } 42 43 Protobuf->Json::{"person": [{"name": "John", "id": 1, "phone": [{"number": "+1234567890", "type": 2, "maps": {"mapfield": [[1, 11], [6, 66]]}}, {"number": "+2345678901", "type": 1}]}, {"name": "Ben Bun", "id": 2, "email": "b@bun.com", "phone": [{"number": "+1234567890", "maps": {"mapfield": [[6, 66], [5, 55]]}}]}]} 44 Protobuf->Byte::b'\n8\n\x04John\x10\x01"\x1d\n\x0b+1234567890\x10\x02\x1a\x0c\n\x04\x08\x01\x10\x0b\n\x04\x08\x06\x10B"\x0f\n\x0b+2345678901\x10\x01\n3\n\x07Ben Bun\x10\x02\x1a\tb@bun.com"\x1b\n\x0b+1234567890\x1a\x0c\n\x04\x08\x06\x10B\n\x04\x08\x05\x107' 45 Byte->Protobuf::person { 46 name: "John" 47 id: 1 48 phone { 49 number: "+1234567890" 50 type: WORK 51 maps { 52 mapfield { 53 key: 1 54 value: 11 55 } 56 mapfield { 57 key: 6 58 value: 66 59 } 60 } 61 } 62 phone { 63 number: "+2345678901" 64 type: HOME 65 } 66 } 67 person { 68 name: "Ben Bun" 69 id: 2 70 email: "b@bun.com" 71 phone { 72 number: "+1234567890" 73 maps { 74 mapfield { 75 key: 5 76 value: 55 77 } 78 mapfield { 79 key: 6 80 value: 66 81 } 82 } 83 } 84 } 85 86 Byte Length=111
3.自動化的ProtoBuf編譯
摘自:https://www.cnblogs.com/a00ium/p/14128974.html
在開發過程中,每次更改后必須重新編譯原始文件可能會變得很乏味。要在安裝開發Python軟件包時自動編譯原始文件,我們可以使用該setup.py
腳本。
讓我們創建一個函數,該函數為.proto
目錄中的所有文件生成Protobuf代碼src/interfaces
並將其存儲在下src/generated
:
1 import pathlib 2 import os 3 from subprocess import check_call 4 5 def generate_proto_code(): 6 proto_interface_dir = "./src/interfaces" 7 generated_src_dir = "./src/generated/" 8 out_folder = "src" 9 if not os.path.exists(generated_src_dir): 10 os.mkdir(generated_src_dir) 11 proto_it = pathlib.Path().glob(proto_interface_dir + "/**/*") 12 proto_path = "generated=" + proto_interface_dir 13 protos = [str(proto) for proto in proto_it if proto.is_file()] 14 check_call(["protoc"] + protos + ["--python_out", out_folder, "--proto_path", proto_path])
接下來,我們需要覆蓋develop
命令,以便每次安裝軟件包時都調用該函數:
1 from setuptools.command.develop import develop 2 from setuptools import setup, find_packages 3 4 class CustomDevelopCommand(develop): 5 """Wrapper for custom commands to run before package installation.""" 6 uninstall = False 7 8 def run(self): 9 develop.run(self) 10 11 def install_for_development(self): 12 develop.install_for_development(self) 13 generate_proto_code() 14 15 setup( 16 name='testpkg', 17 version='1.0.0', 18 package_dir={'': 'src'}, 19 cmdclass={ 20 'develop': CustomDevelopCommand, # used for pip install -e ./ 21 }, 22 packages=find_packages(where='src') 23 )
下次我們運行時pip install -e ./
,Protobuf文件將在中自動生成src/generated
。
4.總結
通過對protoc和google.protobuf模塊的學習,讓我又多了解了一種數據格式,和json、xml一樣,但在性能和效率上比后面2者要高出好多,對於網絡I/O、磁盤、內存等資源有限的情況下,用protobuf來作為數據通信的傳輸和存儲是理想的選擇。protobuf的數據格式靈活方便,數據格式一目了然,先期的格式定義,便於后期開發人員的理解。
對於protobuf對象數據填充的心得:
傳統的protobuf對象數據填充,就像目錄樹一樣,先填充父節點(父目錄),再往下填充(子目錄/文件),一層一層直到葉子節點(文件)。根據這邏輯,就可以通過protobuf對象的屬性方法,根據不同的屬性值來判斷嵌套迭代處理不同的邏輯,達到不用修改程序就能填充protobuf對象的靈活通用性。
在填充protobuf對象中,map類型是個特例,它不同於其他任何一種數據類型,且數據的賦值方式也不同,通過key,value的鍵值對存儲,類似python的dict(key可以是int類型,也可以是string類型);當然,map類型的數據,也是可以通過定義子message類型(包括2個field字段),嵌套的形式來實現同等的效果,如:
map形式:
1 map<int32, int32> mapfield = 1;
非map形式:
1 message maps { 2 int32 field1 = 1; 3 int32 field2 = 2; 4 } 5 repeated maps mapfield = 1;