原文http://blog.jobbole.com/107405/
由於最近公司採用protocol buffer(以下簡稱protobuf)來作爲不同應用之間的數據交換,故最近一段時間研究了protobuf相關技術。在這裏分享下。
protobuf是什麼?
protobuf是google旗下的一款平臺無關,語言無關,可擴展的序列化結構數據格式。所以很適合用做數據存儲和作爲不同應用,不同語言之間相互通信的數據交換格式,只要實現相同的協議格式即同一proto文件被編譯成不同的語言版本,加入到各自的工程中去。這樣不同語言就可以解析其他語言通過protobuf序列化的數據。目前官網提供了C++,Python,JAVA,GO等語言的支持。
protobuf 語法定義
要想使用protobuf必須得先定義proto文件。所以得先熟悉protobuf的消息定義的相關語法。下面就來介紹
首先我們先定義一個proto文件,結構如下:
1 2 3 4 5 | message Article { required int32 article_id=1; optional string article_excerpt=2; repeated string article_picture=3; } |
上面我們主要定義了一個消息,這個消息包括文章ID,文章摘要,文章圖片。下面給出消息定義的相關說明
message是消息定義的關鍵字
required 表示這個字段必須的,必須在序列化的時候被賦值。
optional 代表這個字段是可選的,可以爲0個或1個但不能大於1個。
repeated 則代表此字段可以被重複任意多次包括0次。
int32和string是字段的類型。後面是我們定義的字段名。
最後的1,2,3則是代表每個字段的一個唯一的編號標籤,在同一個消息裏不可以重複。這些編號標籤用與在消息二進制格式中標識你的字段,並且消息一旦定義就不能更改。需要說明的是標籤在1到15範圍的採用一個字節進行編碼。所以通常將標籤1到15用於頻繁發生的消息字段。編號標籤大小的範圍是1到229 – 1。此外不能使用protobuf系統預留的編號標籤(19000 -19999)。
當然protobuf支持更多的類型,比如bool,double,float,枚舉,也可以是其他定義過的消息類型譬如前面的消息Article。支持的基本類型如下:
下面讓我們定義一個數據比較多的article.proto文件來再次說明下proto語法的相關內容,起碼通過列子可以更直觀的感受。
# -*- coding: utf-8 -*-
import Article_pb2
from google.protobuf import json_format
from google.protobuf import text_format
article = Article_pb2.Article()
article.article_id = 1 # 必須賦值,不然在序列化得時候會報異常
article.article_excerpt = "文章簡介"
article.article_type = 2
# 內嵌消息操作
author = article.author
author.name = "oliver"
author.phone = "11111111111"
# repeated類型的字段添加
article_picture = article.article_picture
article_picture.append("1.jpg")
article_picture.append("2.jpg")
article.Extensions[Article_pb2.followers_count] = 30 # 給擴展得字段賦值
print article.IsInitialized() # 檢查required字段是否全部被賦值
"""
輸出True
"""
print article.ListFields() # 列出所有字段得一個元組列表
article_binary = article.SerializeToString() # 序列化API
# article.SerializePartialToString() # 也可以序列化消息,只不過它不會檢查required是否被設置,也就是說可以序列化required字段沒有被賦值的情況
with open("article.binary.txt", "wb+") as f: # 保存到文件
f.write(article_binary)
# 反序列化API ParseFromString 此外將ParseFromString換成MergeFromString這個接口來反序列化也可以
another_article = Article_pb2.Article()
another_article.ParseFromString(article_binary)
print(another_article)
"""
article_id: 1
article_excerpt: "\346\226\207\347\253\240\347\256\200\344\273\213"
article_picture: "1.jpg"
article_picture: "2.jpg"
article_type: PAPER
author {
name: "oliver"
phone: "11111111111"
}
[followers_count]: 30
"""
# 消息與json相互轉化, 通過json_format的MessageToJson這個API
article_json = json_format.MessageToJson(article)
print(article_json)
"""
{
"followersCount": 30,
"author": {
"phone": "11111111111",
"name": "oliver"
},
"articleExcerpt": "\u6587\u7ae0\u7b80\u4ecb",
"articleId": 1,
"articleType": "PAPER",
"articlePicture": [
"1.jpg",
"2.jpg"
]
}
"""
# 消息之間互相複製,主要用到CopyFrom 和MergeFrom 2個API
copy_article = Article_pb2.Article()
copy_article.CopyFrom(article)
print(copy_article)
"""
注意運行以下2行註釋代的碼需要把 “article.Extensions[Article_pb2.followers_count] = 30”這行代碼註釋掉。
猜想extension是對原消息得擴展。並不完全屬於Article。譬如執行一下代碼會報article沒有followers_count這個屬性
article.followers_count = 30
google.protobuf.json_format.ParseError: Message type "Article" has no field named "followersCount".
所以將json轉換爲消息類型的時候, 擴展的類型無處安放。
"""
# article_init = json_format.Parse(article_json, article)
#
# print(article_init)
print text_format.MessageToString(another_article)
# oneof操作,會發現當執行 oneof.code2 = "code2"之後,輸出的結果中沒有code1.自動被清除了。
oneof = Article_pb2.Other()
oneof.code1 = "code1"
print(oneof)
"""
code1: "code1"
"""
oneof.code2 = "code2"
print(oneof)
"""
code2: "code2"
"""
# 刪除指定字段的數據
copy_article.ClearField("author")
# 刪除所有數據
copy_article.Clear()
上面proto文件,我們定義了enum枚舉類型,嵌套的消息。甚至對原有的消息進行了擴展,也可以對字段設置默認值。添加註釋等
此外reserved關鍵字主要用於保留相關編號標籤,主要是防止在更新proto文件刪除了某些字段,而未來的使用者定義新的字段時重新使用了該編號標籤。這會引起一些問題在獲取老版本的消息時,譬如數據衝突,隱藏的一些bug等。所以一定要用reserved標記這些編號標籤以保證不會被使用
當我們需要對消息進行擴展的時候,我們可以用extensions關鍵字來定義一些編號標籤供第三方擴展。這樣的好處是不需要修改原來的消息格式。就像上面proto文件,我們用extend關鍵字來擴展。只要擴展的字段編號標籤在extensions定義的範圍裏。
對於基本數值類型,由於歷史原因,不能被protobuf更有效的encode。所以在新的代碼中使用packed=true可以更加有效率的encode。注意packed只能用於repeated 數值類型的字段。不能用於string類型的字段。
在消息Other中我們看到定義了一個oneof關鍵字。這個關鍵字作用比較有意思。當你設置了oneof裏某個成員值時,它會自動清除掉oneof裏的其他成員,也就是說同一時刻oneof裏只有一個成員有效。這常用於你有許多optional字段時但同一時刻只能使用其中一個,就可以用oneof來加強這種效果。但需要注意的是oneof裏的字段不能用required,optional,repeted關鍵字
一般在我們的項目中肯定會有很多消息類型。我們總不能都定義在一個文件中。當一個proto文件需要另一個proto文件的時候,我們可以通過import導入,就像下面這樣:
1 2 3 4 | import "article.proto"; message Book { //定義消息體 } |
protobuf也提供了包的定義,只要在文件開頭定義package關鍵字即可。主要是爲了防止命名衝突,不過對於Python語言在編譯的時候會忽略包名。
1 2 3 4 | package "foo.bar"; message Book { //定義消息體 } |
很多時候我們會修改更新我們定義的proto文件,如果不遵守一定規則的話,修改的後proto文件可能會引發許多異常。在官網上對更新proto有以下幾點要求
1.不能改變已有的任何編號標籤。
2.只能添加optional和repeated的字段。這樣舊代碼能夠解析新的消息,只是那些新添加的字段會被忽略。但是序列化的時候還是會包含哪些新字段。而新代碼無論是舊消息還是新消息都可以解析。
3.非required的字段可以被刪除,但是編號標籤不可以再次被使用,應該把它標記到reserved中去
4.非required可以被轉換爲擴展字段,只要字段類型和編號標籤保持一致
5.相互兼容的類型,可以從一個類型修改爲另一個類型,譬如int32的字段可以修改爲int64
ptotobuf語法相對比較簡單,一般都能很快熟悉上手。這裏只是粗淺的介紹下,更多詳細內容可以參考https://developers.google.com/protocol-buffers/docs/proto。
proto文件編譯
現在我們有了proto文件,需要把它編譯成我們需要的語言,這裏以python爲例。通過以下命令生成我們需要的python代碼,你會發現目錄多了一個article_pb2.py的文件。
1 | protoc -I=. --python_out=. article.proto |
-I 指定搜索proto文件的目錄,這裏指定爲當前目錄。-I 也可以寫成 –proto_path
–python_out 會將生成的python代碼文件放到等號後面指定的目錄,這裏也指定當前目錄。如果需要生成其他語言的代碼譬如java換成–java_out即可。這裏提供一個官網提供的模版,如下
1 | protoc --proto_path=_IMPORT_PATH_ --cpp_out=_DST_DIR_ --java_out=_DST_DIR_ --python_out=_DST_DIR_ _path/to/file_.proto |
最後指定我們要編譯的proto文件。
現在我們有了編譯後的article_pb2.py,加入到我們的項目中去該怎麼用呢?這個時候就需要用到google提供的protobuf python API。 下面我們通過例子來簡單介紹下API的使用
protobuf python api的使用
直接貼代碼來看,詳細的說明都在註釋裏。主要的SerializeToString和ParseFromString2個方法。一個序列化,一個反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | # -*- coding: utf-8 -*- import Article_pb2 from google.protobuf import json_format from google.protobuf import text_format article = Article_pb2.Article() article.article_id = 1 # 必須賦值,不然在序列化得時候會報異常 article.article_excerpt = "文章簡介" article.article_type = 2 # 內嵌消息操作 author = article.author author.name = "oliver" author.phone = "11111111111" # repeated類型的字段添加 article_picture = article.article_picture article_picture.append("1.jpg") article_picture.append("2.jpg") article.Extensions[Article_pb2.followers_count] = 30 # 給擴展得字段賦值 print article.IsInitialized() # 檢查required字段是否全部被賦值 """ 輸出True """ print article.ListFields() # 列出所有字段得一個元組列表 article_binary = article.SerializeToString() # 序列化API # article.SerializePartialToString() # 也可以序列化消息,只不過它不會檢查required是否被設置,也就是說可以序列化required字段沒有被賦值的情況 with open("article.binary.txt", "wb+") as f: # 保存到文件 f.write(article_binary) # 反序列化API ParseFromString 此外將ParseFromString換成MergeFromString這個接口來反序列化也可以 another_article = Article_pb2.Article() another_article.ParseFromString(article_binary) print(another_article) """ article_id: 1 article_excerpt: "\346\226\207\347\253\240\347\256\200\344\273\213" article_picture: "1.jpg" article_picture: "2.jpg" article_type: PAPER author { name: "oliver" phone: "11111111111" } [followers_count]: 30 """ # 消息與json相互轉化, 通過json_format的MessageToJson這個API article_json = json_format.MessageToJson(article) print(article_json) """ { "followersCount": 30, "author": { "phone": "11111111111", "name": "oliver" }, "articleExcerpt": "\u6587\u7ae0\u7b80\u4ecb", "articleId": 1, "articleType": "PAPER", "articlePicture": [ "1.jpg", "2.jpg" ] } """ # 消息之間互相複製,主要用到CopyFrom 和MergeFrom 2個API copy_article = Article_pb2.Article() copy_article.CopyFrom(article) print(copy_article) """ 注意運行以下2行註釋代的碼需要把 “article.Extensions[Article_pb2.followers_count] = 30”這行代碼註釋掉。 猜想extension是對原消息得擴展。並不完全屬於Article。譬如執行一下代碼會報article沒有followers_count這個屬性 article.followers_count = 30 google.protobuf.json_format.ParseError: Message type "Article" has no field named "followersCount". 所以將json轉換爲消息類型的時候, 擴展的類型無處安放。 """ # article_init = json_format.Parse(article_json, article) # # print(article_init) print text_format.MessageToString(another_article) # oneof操作,會發現當執行 oneof.code2 = "code2"之後,輸出的結果中沒有code1.自動被清除了。 oneof = Article_pb2.Other() oneof.code1 = "code1" print(oneof) """ code1: "code1" """ oneof.code2 = "code2" print(oneof) """ code2: "code2" """ # 刪除指定字段的數據 copy_article.ClearField("author") # 刪除所有數據 copy_article.Clear() |
以上主要是通過python來操作protobuf序列化的數據,我們也可以將序列化後的數據通過網絡發給其他應用。通過protobuf序列化的數據體量更小,傳遞效率相比於XML,JSON效率會更高。其他應用也不需要是python,可以是java,c++。只要實現了相同的proto協議,就可以解析發送過來的序列化數據。
以上就是本人對protobuf的理解,有不當之處還請指出,謝謝!