① Protobuf語法介紹
我們先看看官方文檔給出的定義和描述
簡單來講, ProtoBuf 是結構數據序列化方法,可簡單類比於,其具有以下特點:
在protobuf中,協議是由一系列的消息組成的。因此最重要的就是定義通信時使用到的消息格式。一個Protobuf 消息(對應java類),由至少一個欄位(對應Java類屬性)組合而成。
消息的定義很簡單,就是message關鍵字加上消息的名字
欄位定義格式:
限定修飾符 | 數據類型 | 欄位名稱 | = | 欄位編碼
required:
表示是一個必須欄位,必須相對於發送方,在發送消息之前必須設置該欄位的值,對於接收方,必須能夠識別該欄位的意思。發送之前沒有設置required欄位或者無法識別required欄位都會引發編解碼異常,導致消息被丟棄。
optional:
表示是一個可選欄位,可選對於發送方,在發送消息時,可以有選擇性的設置或者不設置該欄位的值。對於接收方,如果能夠識別可選欄位就進行相應的處理,如果無法識別,則忽略該欄位,消息中的其它欄位正常處理。---因為optional欄位的特性,很多介面在升級版本中都把後來添加的欄位都統一的設置為optional欄位,這樣老的版本無需升級程序也可以正常的與新的軟體進行通信,只不過新的欄位無法識別而已,因為並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡。
repeated:
表示該欄位可以包含0~N個元素。其特性和optional一樣,但是每一次可以包含多個值。可以看作是在傳遞一個數組的值。類比於Java這邊的List
protobuf定義了一套基本的數據類型,幾乎都映射了Java語言的基礎數據類型(主要以Java為例)
詳見下面表格
欄位的命名方式與Java的命名方式大致一致
欄位編碼是一個序列化和反序列化的標記值,有了該值,通信雙方才能互相識別對方的欄位。當然相同的編碼值,其限定修飾符和數據類型必須相同。編碼值的取值范圍為 1~2^32(4294967296)
其中 1~15的編碼時間和空間效率都是最高的,編碼值越大,其編碼的時間和空間效率就越低(相對於1-15),當然一般情況下相鄰的2個值編碼效率的是相同的,除非2個值恰好實在4位元組,12位元組,20位元組等的臨界區。比如15和16
1900~2000編碼值為Google protobuf 系統內部保留值,建議不要在自己的項目中使用。protobuf 還建議把經常要傳遞的值把其欄位編碼設置為1-15之間的值
完整的消息定義示例
枚舉的定義和Java 相同,使用 enum 關鍵字,但是有一些限制。
枚舉值必須大於等於0的整數。
使用分號 ; 分隔枚舉變數而不是Java 語言中的逗號 ,
示例:
你可以將其他消息類型用作欄位類型。已我們現在IM的為例,在每一個CSSendLiveRoomMsgReq消息中包含ImMsgBody消息,此時可以在相同的.proto文件中定義一個ImMsgBody消息類型,然後在CSSendLiveRoomMsgReq消息中指定一個ImMsgBody類型的欄位
示例:
如果你的消息中有很多可選欄位, 並且同時至多一個欄位會被設置, 你可以加強這個行為,使用oneof特性節省內存,Oneof欄位就像可選欄位, 除了它們會共享內存, 至多一個欄位會被設置。 設置其中一個欄位會清除其它欄位。
為了在.proto定義Oneof欄位, 你需要在名字前面加上oneof關鍵字, 比如下面例子的ImMsgBody:
oneof的特性
② 了解一下ProtoBuf
我們在進行網路通信調用的時候,總是需要將內存的數據塊經過序列化,轉換成為一種可以通過網路流進行傳輸的格式。而這種格式在經過了傳輸之後再經過序列化,能還原成我們預想中的數據結構。
那麼我們對於這種用於中間網路傳輸的數據格式就有一定的要求。首先它可以准確地描述數據內容,在此基礎上我們則希望它盡量的小。
最開始流行起來的是XML,可擴展標記語言。由於它可以用來標記數據、定義數據類型,所以用戶可以自己定義數據自己的語言,從而讓對不同的數據結構化成統一的格式稱為了可能。
而另外一個我們熟知的則是jsON(JavaScript Object Notation, JS 對象簡譜)。盡管JSON中缺少了XML中的標簽屬性等描述方式,但是足夠簡介和清晰的層次結構使得其成為了必XML更受歡迎的數據交換格式。
同一份數據顯然JSON的數據量比XML所使用的空間更少。那麼空間省略在哪裡呢?一方面是json使用更簡單的字元來定義數據間的關聯關系;另一方面是JSON減少了對數據類型的描述。但是丟少的數據類型再哪裡呢?
以Java中的 OpenFeign 舉例,JSON中缺少的類型定義被定義道程序中的介面中了。當進行序列化與反序列化時,JSON格式並不記錄數據的類型,具體的數據類型在序列化方與反序列化方通過事先約定的介面來進行定義。這樣就減少了信息傳輸過程中的信息量,從而讓數據得以壓縮。
但是JSON由於沒有定義數據類型,所以在傳輸的過程中實際上就都是文本流,那麼這種方法還可以進一步壓縮嗎?
結合上文的討論,我們先說結論:方法是有的,並寫當前的實現方式是ProtoBuf。但在此之前我們先來了解一下ProtoBuf。
我們可以先看看官方給出的定義與描述:
同樣的,ProtoBuf也是一種支持序列化反序列化的方法,並且他具有很多優點:
實際上,ProtoBuf提供了一種通用的數據描述方式,這種定義數據的方式是通用的,就如同JSON或者XML一樣。
接下來我們來來回答本節一開始的問題,針對JSON來說,ProtoBuf是如何將體積變得更小的呢?答案很簡單,就是為數據序列化反序列化提供更多的先驗知識。
本文暫不過度深入ProtoBuf原理,但是可以通過一張圖來進行簡要說明():
ProtoBuf中的數據是按順序進行排列,而整體的結構為若干個field,每一個field中由 Tag-[Length]-Value 組成。Length是可選的,而是否存在Length是通過Tag的類型來決定的。也就是說如果是指定的類型,比如int64,那我們就可以知道Value的長度,也就不用在依靠Length來對其空間進行描述(redis中的壓縮列表也是這個思想)。
那麼field應該對應的是什麼欄位呢?這個則是在序列化與反序列化時在ProtoBuf的服務端與客戶端之間進行預先定義的。而因為提前定義了field的類型、排序,所以field本身可以不用對欄位名、欄位位置進行描述,只需要根據欄位類型選用合適的二進制序列化方法,將欄位本身的value值進行序列化傳輸即可。
稍微總結一下:
ProtoBuf通過對傳輸欄位的名稱、順序進行預定義,從而在傳輸結構中只需要順序的記錄每個欄位的類型標簽和二進制值。
盡管上文和官方中都是以XML或者JSON來對ProtoBuf進行對比。但是因為ProtoBuf本身就是二進制序列化方式,所以從壓縮比上比較感覺有點欺負人。
對應的在Java中二進制常用的序列化器有Kryo和Hessian。但事實上,由於Kryo和Hessian中都需要對Java類名和欄位信息進行存儲。而ProtoBuf則只有Tag-Length-Value的數據對,且Value更是有針對性的特殊編碼,所以空間佔用小的很多。
Kryo是專門針對Java進行優化了的。所以在使用的便捷性上來說Kryo則更加方便。但ProtoBuf是跨平台的,且由於進行了欄位的順序定義,所以似的ProtoBuf定義後的介面是可以向前兼容的(只向後追加欄位),而這種優勢是Kryo所沒有的。
ProtoBuf是跨語言的,使用ProtoBuf的第一步是先定一個 proto 文件 ,而由於ProtoBuf 2和3語言版本的不同,其定義格式會有所不同,具體的細節還是得參考官方文檔:https://developers.google.cn/protocol-buffers/docs/proto3
對於ProtoBuf 3 的定義文檔我們可以按如下方法定義:
其中message關鍵字是定義的文件名,而 string、int32則是預定的欄位類型,repeated則是描述欄位為可重復任意多次的欄位。
ProtoBuf通過這種形式的文件定義了傳輸信息的文件結構。
但是之前小節中我們知道了ProtoBuf是通過 Tag-[Length]-Value 組成的數據組來進行信息傳輸的,那麼proto文件中定義的內容如何轉換為實際傳輸的對象呢?
ProtoBuf的做法是,為每一種語言提供一個生成器protoc。通過使用protoc則可以根據.proto文件生成為一組java文件。對應的官方語法演示樣例為:
官方的生成參考為:https://developers.google.com/protocol-buffers/docs/reference/java-generated
生成後的java文件將提供對應的實體以及數據的構造方法等文件,從而支持後續的使用。
需要注意的是,ProtoBuf是本質上是序列化方法,具體是通過Spring Cloud 的OpenFeign進行介面調用,還是通過grpc進行介面調用,都是可以的。
本文對ProtoBuff進行了概念的整理,並沒有對每個細節都進行深入的梳理,可以當作概念科普來進行閱讀。
③ protobuf3鍩虹璇娉
ProtoBuf 錛圙oogle Protocol Buffer錛夋槸鐢眊oogle鍏鍙哥敤浜庢暟鎹浜ゆ崲鐨勫簭鍒楃粨鏋勫寲鏁版嵁鏍煎紡錛屽叿鏈夎法騫沖彴銆佽法璇璦銆佸彲鎵╁睍鐗規э紝鍚岀被鍨嬫湁甯哥敤鐨刋ML鍙奐SON錛屼絾鍏鋒湁鏇村皬鐨勪紶杈撲綋縐銆佹洿楂樼殑緙栫爜銆佽В鐮佽兘鍔涳紝鐗瑰埆閫傚悎浜庢暟鎹瀛樺偍銆佺綉緇滄暟鎹浼犺緭絳夊瑰瓨鍌ㄤ綋縐銆佸疄鏃舵ц佹眰楂樼殑棰嗗煙錛岀洰鍓嶅凡緇忓彂灞曞埌protoc3 鐗堟湰銆
浼樼偣錛氱┖闂存晥鐜囬珮錛屾椂闂存晥鐜囪侀珮錛屽逛簬鏁版嵁澶у皬鏁忔劅錛屼紶杈撴晥鐜囬珮鐨
緙虹偣錛氭秷鎮緇撴瀯鍙璇繪т笉楂橈紝搴忓垪鍖栧悗鐨勫瓧鑺傚簭鍒椾負浜岃繘鍒跺簭鍒椾笉鑳界畝鍗曠殑鍒嗘瀽鏈夋晥鎬
澶囨敞錛氭渶鍚庣殑鏃墮棿綾誨瀷golang闇瑕佸紩鍏ュ寘 github.com/golang/protobuf/ptypes/timestamp ,瀹氫箟濡備笅
鐒跺悗 .protp 鏂囦歡闇瑕佸煎叆 google/protobuf/timestamp.proto
濡傛灉涓涓瀛楁佃 repeated 淇楗幫紝鍒欒〃紺哄畠鏄涓涓鍒楄〃綾誨瀷鐨勫瓧孌碉紝鐩稿綋浜 golang 閲岀殑鍒囩墖
濡傛灉浣犲笇鏈涘彲浠ラ勭暀涓浜涙暟瀛楁爣絳炬垨鑰呭瓧孌靛彲浠ヤ嬌鐢╮eserved淇楗扮
絎涓涓鏋氫婦鍊肩殑鏁板煎繀欏繪槸0涓旇嚦灝戞湁涓涓鏋氫婦鍊礆紝涓涓鏁板煎彲浠ュ瑰簲澶氫釜鏋氫婦鍊礆紝蹇呴』鏍囨槑 option allow_alias = true; 涓嶆帹鑽愪嬌鐢ㄨ礋鏁板
鍦ㄤ綘鐨 .proto 鏂囦歡涓鎸囧畾 service ,鐒跺悗鍦 service 閲屽畾涔 rpc鏂規硶 鍗沖彲錛岃佹敞鎰忔寚瀹氬弬鏁板拰榪斿洖鍊
gRPC 鍏佽鎬綘瀹氫箟4縐嶇被鍨嬬殑 service 鏂規硶
瀹㈡埛絝浣跨敤瀛樻牴鍙戦佽鋒眰鍒版湇鍔″櫒騫剁瓑寰呭搷搴旇繑鍥烇紝灝卞儚騫沖父鐨勫嚱鏁拌皟鐢ㄤ竴鏍
閫氳繃鍦 鍝嶅簲榪斿洖鍙傛暟 綾誨瀷鍓嶆彃鍏 stream 鍏抽敭瀛楋紝鍙浠ユ寚瀹氫竴涓鏈嶅姟鍣ㄧ鐨勬祦鏂規硶銆傚㈡埛絝鍙戦佽鋒眰鍒版湇鍔″櫒錛屾嬁鍒頒竴涓嫻佸幓璇誨彇榪斿洖鐨勬秷鎮搴忓垪銆 瀹㈡埛絝璇誨彇榪斿洖鐨勬祦錛岀洿鍒伴噷闈㈡病鏈変換浣曟秷鎮銆
閫氳繃鍦 璇鋒眰鍙傛暟 綾誨瀷鍓嶆寚瀹 stream 鍏抽敭瀛楁潵鎸囧畾涓涓瀹㈡埛絝鐨勬祦鏂規硶銆傚㈡埛絝鍐欏叆涓涓娑堟伅搴忓垪騫跺皢鍏跺彂閫佸埌鏈嶅姟鍣錛屽悓鏍蜂篃鏄浣跨敤嫻併備竴鏃﹀㈡埛絝瀹屾垚鍐欏叆娑堟伅錛屽畠絳夊緟鏈嶅姟鍣ㄥ畬鎴愯誨彇榪斿洖瀹冪殑鍝嶅簲銆
閫氳繃鍦ㄨ鋒眰鍜屽搷搴斿墠鍔 stream 鍏抽敭瀛楀幓鍒跺畾鏂規硶鐨勭被鍨嬨備袱涓嫻佺嫭絝嬫搷浣滐紝鍥犳ゅ㈡埛絝鍜屾湇鍔″櫒鍙浠ヤ互浠繪剰鍠滄㈢殑欏哄簭璇誨啓錛氭瘮濡傦紝 鏈嶅姟鍣ㄥ彲浠ュ湪鍐欏叆鍝嶅簲鍓嶇瓑寰呮帴鏀舵墍鏈夌殑瀹㈡埛絝娑堟伅錛屾垨鑰呭彲浠ヤ氦鏇跨殑璇誨彇鍜屽啓鍏ユ秷鎮錛屾垨鑰呭叾浠栬誨啓鐨勭粍鍚堛