Protobuf第三方擴展開發(fā)指南

谷歌Protobuf目前最新3.6.1版本官方支持許多語言:C++朽砰、Java朦佩、Python并思、Go等等。除官方支持外這里還列出了許多開源的第三方擴展语稠。

如果官方不支持自己的語言宋彼,各種開源庫也不能滿足需求,那么就需要自己動手編寫第三方擴展來支持目標語言仙畦。

本文介紹如何為Protobuf編寫第三方擴展输涕。注意:

  • 本文不討論谷歌RPC
  • 本文假設(shè)讀者已經(jīng)了解Protobuf的基本用法,如果你還不會用那么本文不適合你閱讀

第1章 了解Protobuf

Protobuf分為編譯器和運行時兩個部分慨畸。本章介紹Protobuf是如何工作的莱坎。

1.1 Protobuf是如何運行的

通常我們編寫好xxx.proto文件后需要:

  • 編譯:將proto文件編譯生成目標碼,不同目標語言的目標碼形式都不相同寸士。
    以Python為例型奥,protoc --python_out=/path/to/output/ xxx.proto會生成xxx_pb2.py瞳收。
    protoc可以在GitHub的release頁面下載,例如:protoc-3.6.1-linux-x86_64.zip厢汹。
  • 運行:將生成的目標碼拷貝至目標機運行,要求目標機上有對應(yīng)的運行時谐宙。
    以Python為例烫葬,Python應(yīng)用程序中可以直接調(diào)用:import xxx_pb2
    運行需要依賴Protobuf的Python運行時凡蜻,可以在release頁面下載搭综,例如:protobuf-python-3.6.1.tar.gz。

圖1-1 顯示了Protobuf是如何工作的划栓,也有的時候編譯兑巾、運行是在同一個機器上進行的。


圖1-1 Protobuf工作流程

1.2 編寫第三方擴展的幾種方案

讀到這應(yīng)該也猜到了忠荞,我們需要編寫編譯器和運行時兩個部分蒋歌。運行時沒什么好說的,可以參考后續(xù)的Demo委煤。編譯器分為前端和后端兩個部分堂油,見圖1-2。

圖1-2 編譯器前端碧绞、后端府框、目標碼

谷歌已經(jīng)實現(xiàn)了編譯器前端,在此基礎(chǔ)上我們只需要編寫編譯器后端即可:

  • 方案一:在谷歌CommandLineInterface接口的基礎(chǔ)上進一步開發(fā)讥邻,該接口封裝了Protobuf編譯器前端迫靖,在此基礎(chǔ)上你可以輕松實現(xiàn)編譯器后端;你必須使用C++語言開發(fā)后端兴使;
  • 方案二:編寫插件來實現(xiàn)系宜,Protobuf 3.0開始支持插件,這是目前最推薦的方式鲫惶;
  • 方案三:自己編寫編譯器前端和后端蜈首,如果沒有特殊需求的話非常不推薦這種方式,詳見下文欠母;

第2章 三種方案快速入門

2.1 方案一:通過C++接口實現(xiàn)編譯器后端(不推薦)

早期Protobuf 2不支持插件欢策,因此一些較老的開源項目是使用此方案,如:

  • protobuf-c

Protobuf 3開始已經(jīng)支持插件了赏淌,我們推薦使用插件實現(xiàn)踩寇,本節(jié)的技術(shù)點算是過時了。因此這一節(jié)的內(nèi)容被我移到這篇文章了六水。

2.2 方案二:通過編寫插件實現(xiàn)編譯器后端(推薦)

編譯器后端無非就是獲取proto語法樹俺孙,然后進行生成辣卒。在方案一中,語法樹是通過C++的一個類來表達的睛榄,這樣就導(dǎo)致后端代碼需要依賴谷歌C++的頭文件和庫荣茫,兼容性較差。

而方案二是通過proto數(shù)據(jù)流來表達語法樹的场靴,后端只要依賴相應(yīng)語言的Protobuf庫即可啡莉。這是兼容性最好的方案。由于Protobuf官方就支持:C++旨剥、Dart咧欣、Go、Java轨帜、Python魄咕、Ruby、C#蚌父、OC哮兰、Javascript、PHP梢什,因此無論使用上述哪種語言都可以用來開發(fā)編譯器后端插件奠蹬。

2.2.1 一個哲學(xué)概念

proto代碼定義的是信息的模型,例如下面一段proto代碼定義的就是人的信息模型:

syntax = "proto3";
package demo;

message Person {
    string name = 1;
    int32 id = 2;
    string email = 3;
}

而這段信息模型對應(yīng)的一個可能的信息內(nèi)容是:

張三, 112233, zhangsan@163.com

這樣我們就知道信息模型是信息內(nèi)容的抽象泛化嗡午,它們處于不同層次囤躁。就好像我們可以通過橡皮泥模子捏出各種顏色的橡皮泥。那么信息模型就好比橡皮泥模子荔睹,而信息內(nèi)容就好比捏出來的橡皮泥狸演。


image.png

然而proto代碼自身也可以看做是信息,它也能有對應(yīng)的模型僻他,即:proto代碼是proto代碼的模型宵距,這聽起來真的很繞,自己怎么就成了自己的模型了呢吨拗。其實更詳細的說應(yīng)該是:有一種特殊的proto代碼满哪,它是其它任意proto代碼的模型,這個特殊的proto代碼就是descriptor.proto劝篷。這真的很哲學(xué)哨鸭,從某角度來看descriptor.proto也是proto代碼,它和普通proto代碼是同一個層次的娇妓;但從另一個角度來看descriptor.proto能作為所有proto代碼的模型像鸡,它又和普通proto代碼不在同一個層次。

descriptor.proto的這種現(xiàn)象在計算機科學(xué)中叫做「元(meta)」哈恰,例如:

  • 在Python中我們用一個類來定義其它類只估,這叫元類
  • 在Lua中我們用一個表來定義其它表志群,這叫元表
  • 類似的詞匯你可能還聽過很多,例如:元編程

幾乎毫無例外的蛔钙,各個技術(shù)領(lǐng)域出現(xiàn)的「元」的概念都成為最難理解的知識點之一锌云,元本身的概念超出了本文范圍。

2.2.2 插件的運行流程

在執(zhí)行protoc編譯的時候夸楣,命令行是這樣寫的:protoc -python_out=./ *.proto宾抓,Protobuf原生支持Python所以認識-python_out,這表示把當前目錄下所有proto都編譯成python豫喧。如果我們命令行這樣寫:protoc -xxx_out=./ *.proto,由于不認識xxx幢泼,于是protoc會在PATH路徑下尋找一個叫做protoc-gen-xxx的可執(zhí)行文件紧显。而protoc-gen-xxx就是我們要實現(xiàn)的插件。

插件的運行流程如圖:

  • 我們只需要關(guān)注步驟3.1~3.5缕棵,其余步驟是谷歌protoc完成
  • CodeGeneratorRequest和CodeGeneratorResponse對象定義在plugin.proto里面
  • FileDescriptor代表了一個proto文件孵班,定義在descriptor.proto里面
image.png

2.2.3 插件具體實現(xiàn)代碼

本章概述就說了插件可以通過多種語言(Python、Java招驴、C#等)實現(xiàn)篙程,這里我們以Python為例。實現(xiàn)涉及到的主要類為:

  • CodeGeneratorRequest對應(yīng):from google.protobuf.compiler.plugin_pb2 import CodeGeneratorRequest
  • CodeGeneratorResponse對應(yīng):from google.protobuf.compiler.plugin_pb2 import CodeGeneratorResponse
  • FileDescriptor對應(yīng):from google.protobuf.descriptor import FileDescriptor

一個簡單的Demo代碼如下别厘,將代碼命名為protoc-gen-hello虱饿,賦予可執(zhí)行權(quán)限,然后放置到PATH路徑下:

#!/usr/bin/env python3
import sys
from google.protobuf.compiler import plugin_pb2

# 3.1 讀取二進制
input_data = sys.stdin.buffer.read()

# 3.2 反序列化
req = plugin_pb2.CodeGeneratorRequest.FromString(input_data)

# 3.3 根據(jù)業(yè)務(wù)需求具體實現(xiàn)
# req.proto_file[0]是FileDescriptor類型的對象
# 正常業(yè)務(wù)肯定要通過req.proto_file[0]讀取proto代碼的信息触趴,然后根據(jù)具體業(yè)務(wù)需求解析
# 但這段代碼只是為了簡單演示氮发,就不讀取了

# 3.4 構(gòu)造CodeGeneratorResponse對象
# 下面這段邏輯說明,不管輸入的proto如何
# 本插件都會輸出aaa.txt和bbb.txt文件冗懦,文件內(nèi)容都是hello
resp = plugin_pb2.CodeGeneratorResponse()
resp.file.add()
resp.file[0].name = 'aaa.txt'
resp.file[0].content = 'hello'
resp.file.add()
resp.file[1].name = 'bbb.txt'
resp.file[1].content = 'hello'

# 3.5 將CodeGeneratorResponse序列化成二進制后打印
sys.stdout.buffer.write(resp.SerializeToString())

為了運行這段代碼爽冕,我們的命令是:protoc --hello_out=./ *.proto,這樣protoc會去尋找一個叫做protoc-gen-hello的可執(zhí)行文件披蕉。

2.3 方案三:自己編寫編譯器前端颈畸、后端

此法風(fēng)險和難度比較大:

  • 需要開發(fā)者熟練掌握編譯原理
  • 谷歌并沒有對此方案提供任何技術(shù)支持
  • Protobuf有一些隱含語法,這部分并沒有在官方文檔中說明没讲,讓此方案的兼容性得不到保障

如果有特殊需求才考慮此法:

  • 有proto文件熱加載需求眯娱,即希望應(yīng)用能夠直接加載proto文件

目前也有一些開源庫使用此方案:

2.4 方案對比

方案一只能通過C++實現(xiàn)。如果不是遺留項目食零,方案一沒有什么優(yōu)勢困乒。

方案二可選開發(fā)語言多樣,推薦使用贰谣。

方案三難度最大娜搂,基本只有特殊需求迁霎、學(xué)習(xí)研究會考慮此法。

第三章 Protobuf插件開發(fā)詳解

在2.2節(jié)中已經(jīng)介紹了基本內(nèi)容百宇,因為從Protobuf 3開始這是最常用的方法考廉,所以這里花一個章節(jié)的篇幅詳細介紹。

本章以Python語言為例編寫一個簡易的Protobuf插件携御,該插件的名字是protoc-gen-lint昌粤,用來檢測proto代碼是否有潛在問題。我們知道一個成熟的lint工具檢測內(nèi)容是非常多的啄刹,甚至包括英語單詞拼寫是否正確涮坐,但是本章作為教學(xué)例子,只做了非常有限的幾個功能誓军。

本章例子的檢測的內(nèi)容是:

  • 判斷message名是否為駝峰命名法袱讹,即不能含有下劃線、不能有兩個連續(xù)大寫字母出現(xiàn)
  • 等等

3.1 代碼如下

#!/usr/bin/env python3
import sys
from google.protobuf.compiler import plugin_pb2

input_data = sys.stdin.buffer.read()
req = plugin_pb2.CodeGeneratorRequest.FromString(input_data)
resp = plugin_pb2.CodeGeneratorResponse()

for f in req.proto_file:
    log = ''
    for m in f.message_type:
        if '_' in m.name:
            # 判斷是否存在下劃線
            log += '{0}中的{1}不符合駝峰命名法\n'.format(f.name, m.name)
            break
        else:
            # 判斷是否存在連續(xù)的大寫字母
            for i in range(len(m.name) - 1):
                if m.name[i].isupper() and m.name[i+1].isupper():
                    log += '{0}中的{1}不符合駝峰命名法\n'.format(f.name, m.name)
                    break
    if log != '':
        # 若確實存在問題昵时,則將報錯信息輸出到.lint.txt后綴的文件中
        resp.file.add()
        resp.file[-1].name = f.name.rstrip('.proto') + '.lint.txt'
        resp.file[-1].content = log

sys.stdout.buffer.write(resp.SerializeToString())

附錄

附錄:官方支持語言列表

附錄:供參考的開源項目

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捷雕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壹甥,更是在濱河造成了極大的恐慌救巷,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件句柠,死亡現(xiàn)場離奇詭異浦译,居然都是意外死亡,警方通過查閱死者的電腦和手機俄占,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門管怠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缸榄,你說我怎么就攤上這事渤弛。” “怎么了甚带?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵她肯,是天一觀的道長。 經(jīng)常有香客問我鹰贵,道長晴氨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任碉输,我火速辦了婚禮籽前,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己枝哄,他們只是感情好肄梨,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挠锥,像睡著了一般众羡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蓖租,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天粱侣,我揣著相機與錄音,去河邊找鬼蓖宦。 笑死齐婴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的稠茂。 我是一名探鬼主播尔店,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼主慰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鲫售,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤共螺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后情竹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藐不,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年秦效,在試婚紗的時候發(fā)現(xiàn)自己被綠了雏蛮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡阱州,死狀恐怖挑秉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苔货,我是刑警寧澤犀概,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站夜惭,受9級特大地震影響姻灶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诈茧,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一产喉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦曾沈、人聲如沸这嚣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疤苹。三九已至,卻和暖如春敛腌,著一層夾襖步出監(jiān)牢的瞬間卧土,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工像樊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尤莺,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓生棍,卻偏偏與公主長得像颤霎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涂滴,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容