由於最近公司採用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語法的相關內容,起碼通過列子可以更直觀的感受。
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 |
syntax = "proto2"; message Article { required int32 article_id = 1; optional string article_excerpt = 2; repeated string article_picture = 3; optional int32 article_pagecount = 4 [default = 0]; enum ArticleType { NOVEL = 0; PROSE = 1; PAPER = 2; POETRY = 3; } optional ArticleType article_type = 5 [default = NOVEL]; message Author { required string name = 1; //作者的名字 optional string phone = 2; } optional Author author = 6; repeated int32 article_numberofwords = 7 [packed=true]; reserved 9, 10, 12 to 15; extensions 100 to 1000; } extend Article { optional int32 followers_count = 101; optional int32 likes_count= 102; } message Other { optional string other_info = 1; oneof test_oneof { string code1 = 2; string code2 = 3; } } |
上面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的理解,有不當之處還請指出,謝謝!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式