谷歌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.2 編寫第三方擴展的幾種方案
讀到這應(yīng)該也猜到了忠荞,我們需要編寫編譯器和運行時兩個部分蒋歌。運行時沒什么好說的,可以參考后續(xù)的Demo委煤。編譯器分為前端和后端兩個部分堂油,見圖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)容就好比捏出來的橡皮泥狸演。
然而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里面
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())