協議緩沖區(Protobuf)是Google開發的與語言無關的數據序列化格式。Protobuf之所以出色,原因如下:
- 數據量低: Protobuf使用二進制格式,該格式比JSON等其他格式更緊湊。
- 持久性: Protobuf序列化是向后兼容的。這意味着即使接口在此期間發生了更改,您也可以始終還原以前的數據。
- 按合同設計: Protobuf要求使用顯式標識符和類型來規范消息。
- gRPC的要求: gRPC(gRPC遠程過程調用)是一種利用Protobuf格式的高效遠程過程調用系統。
就個人而言,我最喜歡Protobuf的是,如果強迫開發人員明確定義應用程序的接口。這是一個改變規則的游戲,因為它使所有利益相關者都能理解界面設計並為之做出貢獻。
在這篇文章中,我想分享我在Python應用程序中使用Protobuf的經驗。
安裝Protobuf
對於大多數系統,Protobuf必須從源代碼安裝。在下面,我描述了Unix系統的安裝:
1.從Git下載最新的Protobuf版本:
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.4/protobuf-all-3.12.4.tar.gz
2.解壓縮
tar -xzf protobuf-all-3.12.4.tar.gz
3.安裝:
cd protobuf-3.12.4/ && ./configure && make && sudo make install
4.驗證安裝(protoc
現在應該可用!)
protoc
protoc --version
一旦原型編譯器可用,我們就可以開始。
1. Protobuf消息的定義
要使用Protobuf,我們首先需要定義我們要傳輸的消息。消息在.proto
文件內定義。請考慮官方文檔以獲取協議緩沖區語言的詳細信息。在這里,我僅提供一個簡單的示例,旨在展示最重要的語言功能。
假設我們正在開發一個類似Facebook的社交網絡,該社交網絡完全是關於人及其聯系的。這就是為什么我們要為一個人建模消息。
一個人具有某些固有的特征(例如年齡,性別,身高),還具有我們需要建模的某些外在特征(例如朋友,愛好)。讓我們存儲以下定義src/interfaces/person.proto
:
syntax = "proto3"; import "generated/person_info.proto"; package persons; message Person { PersonInfo info = 1; // characteristics of the person repeated Friend friends = 2; // friends of the person } message Friend { float friendship_duration = 1; // duration of friendship in days repeated string shared_hobbies = 2; // shared interests Person person = 3; // identity of the friend }
請注意,我們引用的是另一個原始文件,generated/person_info.proto
我們將其定義為:
syntax = "proto3"; package persons; enum Sex { M = 0; // male F = 1; // female O = 2; // other } message PersonInfo { int32 age = 1; // age in years Sex sex = 2; int32 height = 3; // height in cm }
不用擔心這些定義對您還沒有意義,我現在將解釋最重要的關鍵字:
- 語法:語法定義了規范使用哪個版本的Protobuf。我們正在使用
proto3
。 - import:如果根據另一條消息定義了一條消息,則需要使用
import
語句將其包括在內。您可能想知道為什么導入person.proto
?我們稍后將對此進行更深入的研究-現在僅知道這是由於Python的導入系統所致。generated/person_info.proto
interfaces/person_info.proto
- package:包定義了屬於同一名稱空間的消息。這樣可以防止名稱沖突。
- enum:一個枚舉定義一個枚舉類型。
- messsage:消息是我們想使用Protobuf建模的一條信息。
- repeat:
repeated
關鍵字指示一個變量,該變量被解釋為向量。在我們的情況下,friends
是Friend
消息的向量。
還要注意,每個消息屬性都分配有一個唯一的編號。該編號對於協議的向后兼容是必需的:一旦將編號分配給字段,則不應在以后的時間點對其進行修改。
現在我們有了應用程序的基本原型定義,我們可以開始生成相應的Python代碼了。
2.原始文件的編譯
要將原始文件編譯為Python對象,我們將使用Protobuf編譯器protoc
。
我們將使用以下選項調用原型編譯器:
--python_out
:將存儲已編譯的Python文件的目錄--proto_path
:由於原始文件不在項目的根文件夾中,因此我們需要使用替代文件。通過指定generated=./src/interfaces
,編譯器知道在導入其他原始消息時,我們要使用生成文件的路徑(generated
),而不是接口的位置(src/interfaces
)。
有了這種了解,我們可以像下面這樣編譯原始文件:
mkdir src/generated protoc src/interfaces/person_info.proto --python_out src/ --proto_path generated=./src/interfaces/ protoc src/interfaces/person.proto --python_out src/ --proto_path generated=./src/interfaces/
執行完這些命令后,文件generated/person_pb2.py
和generated/person_info_pb2.py
應該存在。例如, person_pb2.py
如下所示:
_PERSON = _descriptor.Descriptor(
name='Person', full_name='persons.Person', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ ...

生成的Python代碼並非真正可讀。但這沒關系,因為我們只需要知道person_pb2.py
可以用於構造可序列化的Protobuf對象即可。
3. Protobuf對象的序列化
在我們以有意義的方式序列化Protobuf對象之前,我們需要用一些數據填充它。讓我們生成一個有一個朋友的人:
# fill protobuf objects import generated.person_pb2 as person_pb2 import generated.person_info_pb2 as person_info_pb2 ############ # define friend for person of interest ############# friend_info = person_info_pb2.PersonInfo() friend_info.age = 40 friend_info.sex = person_info_pb2.Sex.M friend_info.height = 165 friend_person = person_pb2.Person() friend_person.info.CopyFrom(friend_info) friend_person.friends.extend([]) # no friends :-( ####### # define friendship characteristics ######## friendship = person_pb2.Friend() friendship.friendship_duration = 365.1 friendship.shared_hobbies.extend(["books", "daydreaming", "unicorns"]) friendship.person.CopyFrom(friend_person) ####### # assign the friend to the friend of interest ######### person_info = person_info_pb2.PersonInfo() person_info.age = 30 person_info.sex = person_info_pb2.Sex.M person_info.height = 184 person = person_pb2.Person() person.info.CopyFrom(person_info) person.friends.extend([friendship]) # person with a single friend
請注意,我們通過直接分配填充了所有瑣碎的數據類型(例如,整數,浮點數和字符串)。僅對於更復雜的數據類型,才需要使用其他一些功能。例如,我們利用extend
來填充重復的Protobuf字段並CopyFrom
填充Protobuf子消息。
要序列化Protobuf對象,我們可以使用SerializeToString()
函數。此外,我們還可以使用以下str()
函數將Protobuf對象輸出為人類可讀的字符串:
# serialize proto object import os out_dir = "proto_dump" with open(os.path.join(out_dir, "person.pb"), "wb") as f: # binary output f.write(person.SerializeToString()) with open(os.path.join(out_dir, "person.protobuf"), "w") as f: # human-readable output for debugging f.write(str(person))
執行完代碼段后,可以在proto_dump/person.protobuf
以下位置找到生成的人類可讀的Protobuf消息:
info {
age: 30
height: 184
}
friends {
friendship_duration: 365.1000061035156
shared_hobbies: "books"
shared_hobbies: "daydreaming"
shared_hobbies: "unicorns"
person {
info {
age: 40
height: 165
}
}
}
請注意,此人的信息既不顯示該人的性別,也不顯示其朋友的性別。這不是Bug,而是Protobuf的功能:0
永遠不會打印值為的條目。sex
由於這兩個人都是男性,因此此處未顯示0
。
4.自動化的Protobuf編譯
在開發過程中,每次更改后必須重新編譯原始文件可能會變得很乏味。要在安裝開發Python軟件包時自動編譯原始文件,我們可以使用該setup.py
腳本。
讓我們創建一個函數,該函數為.proto
目錄中的所有文件生成Protobuf代碼src/interfaces
並將其存儲在下src/generated
:
import pathlib import os from subprocess import check_call def generate_proto_code(): proto_interface_dir = "./src/interfaces" generated_src_dir = "./src/generated/" out_folder = "src" if not os.path.exists(generated_src_dir): os.mkdir(generated_src_dir) proto_it = pathlib.Path().glob(proto_interface_dir + "/**/*") proto_path = "generated=" + proto_interface_dir protos = [str(proto) for proto in proto_it if proto.is_file()] check_call(["protoc"] + protos + ["--python_out", out_folder, "--proto_path", proto_path])
接下來,我們需要覆蓋develop
命令,以便每次安裝軟件包時都調用該函數:
from setuptools.command.develop import develop from setuptools import setup, find_packages class CustomDevelopCommand(develop): """Wrapper for custom commands to run before package installation.""" uninstall = False def run(self): develop.run(self) def install_for_development(self): develop.install_for_development(self) generate_proto_code() setup( name='testpkg', version='1.0.0', package_dir={'': 'src'}, cmdclass={ 'develop': CustomDevelopCommand, # used for pip install -e ./ }, packages=find_packages(where='src') )
下次我們運行時pip install -e ./
,Protobuf文件將在中自動生成src/generated
。
我們節省多少空間?
之前,我提到Protobuf的優點之一是其二進制格式。在這里,我們將通過比較Protobuf消息的大小和Person
相應的JSON來考慮此優勢:
"person": { "info": { "age": 30, "height": 184 }, "friends": { "friendship_duration": 365.1000061035156, "shared_hobbies": ["books", "daydreaming", "unicorns"], "person": { "info": { "age": 40, "height": 165 } } } }
比較JSON和Protobuf文本表示形式,結果發現JSON實際上更緊湊,因為它的列表表示形式更加簡潔。但是,這令人產生誤解,因為我們對二進制Protobuf格式感興趣。
當比較Person
對象的二進制Protobuf和JSON占用的字節數時,我們發現以下內容:
du -b person.pb
53 person.pb
du -b person.json
304 person.json
在這里,Protobuf比JSON小5倍
二進制Protobuf(53個字節)比相應的JSON(304個字節)小5倍以上。請注意,如果我們使用gRPC協議傳輸二進制Protobuf,則只能達到此壓縮級別。
如果不選擇gRPC,則常見的模式是使用base64編碼對二進制Protobuf數據進行編碼。盡管此編碼不可撤銷地將有效載荷的大小增加了33%,但仍比相應的REST有效載荷小得多。
概要
Protobuf是數據序列化的理想格式。它比JSON小得多,並且允許接口的顯式定義。由於其良好的性能,我建議在所有使用足夠復雜數據的項目中使用Protobuf。盡管Protobuf需要初步的時間投入,但我敢肯定它會很快得到回報。