數據交換利器 Protobuf 技術淺析

原文http://blog.jobbole.com/107405/

由於最近公司採用protocol buffer(以下簡稱protobuf)來作爲不同應用之間的數據交換,故最近一段時間研究了protobuf相關技術。在這裏分享下。

protobuf是什麼?

protobuf是google旗下的一款平臺無關,語言無關,可擴展的序列化結構數據格式。所以很適合用做數據存儲和作爲不同應用,不同語言之間相互通信的數據交換格式,只要實現相同的協議格式即同一proto文件被編譯成不同的語言版本,加入到各自的工程中去。這樣不同語言就可以解析其他語言通過protobuf序列化的數據。目前官網提供了C++,Python,JAVA,GO等語言的支持。

protobuf 語法定義

要想使用protobuf必須得先定義proto文件。所以得先熟悉protobuf的消息定義的相關語法。下面就來介紹

首先我們先定義一個proto文件,結構如下:

上面我們主要定義了一個消息,這個消息包括文章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導入,就像下面這樣:

protobuf也提供了包的定義,只要在文件開頭定義package關鍵字即可。主要是爲了防止命名衝突,不過對於Python語言在編譯的時候會忽略包名。

很多時候我們會修改更新我們定義的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的文件。

-I 指定搜索proto文件的目錄,這裏指定爲當前目錄。-I 也可以寫成 –proto_path

–python_out 會將生成的python代碼文件放到等號後面指定的目錄,這裏也指定當前目錄。如果需要生成其他語言的代碼譬如java換成–java_out即可。這裏提供一個官網提供的模版,如下

最後指定我們要編譯的proto文件。

現在我們有了編譯後的article_pb2.py,加入到我們的項目中去該怎麼用呢?這個時候就需要用到google提供的protobuf python API。 下面我們通過例子來簡單介紹下API的使用

protobuf python api的使用

直接貼代碼來看,詳細的說明都在註釋裏。主要的SerializeToString和ParseFromString2個方法。一個序列化,一個反序列化。

以上主要是通過python來操作protobuf序列化的數據,我們也可以將序列化後的數據通過網絡發給其他應用。通過protobuf序列化的數據體量更小,傳遞效率相比於XML,JSON效率會更高。其他應用也不需要是python,可以是java,c++。只要實現了相同的proto協議,就可以解析發送過來的序列化數據。

以上就是本人對protobuf的理解,有不當之處還請指出,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章