本文主要針對Protobuf進(jìn)行介紹,主要針對版本proto2特漩,給出demo來講解proto語法,并對其中部分編解碼原理進(jìn)行講解,最后進(jìn)行總結(jié)和思考
介紹
官網(wǎng) https://developers.google.com/protocol-buffers/docs/overview
Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the "old" format.
特點:
靈活高效骨杂,自動的序列化涂身,反序列化機(jī)制,定義IDL搓蚪,生成代碼蛤售,支持跨語言,有很好的兼容性
適用場景
**Protocol buffers 很適合做數(shù)據(jù)存儲或 RPC 數(shù)據(jù)交換格式°材埽可用于通訊協(xié)議揣钦、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)搜骡、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)格式**拂盯。
歷史背景
proto2 和 proto3 的名字看起來有點撲朔迷離佑女,那是因為當(dāng)我們最初開源的 protocol buffers 時记靡,它實際上是 Google 的第二個版本了,所以被稱為 proto2团驱,這也是我們的開源版本號從 v2 開始的原因摸吠。初始版名為 proto1,從 2001 年初開始在谷歌開發(fā)的嚎花。
現(xiàn)實應(yīng)用: grpc
Demo
proto文件
proto文件寸痢,類似idl定義,下面文件是a.proto
`syntax="proto2";`
`package main;`
`message Person {`
`required string name = 1;`
`required int32 age = 2;`
` }`
` message Person1 {`
`required string name = 1;`
`optional int32 age = 2;`
` }`
main.go
package main
import (
"fmt"
"io/ioutil"
"github.com/golang/protobuf/proto"
)
//go run *.go
//protoc --go_out . a.proto
/**
message Person {
required string name = 1;
required int32 age = 2;
}
*/
func main() {
//testPerson()
testPerson1()
}
func testPerson() {
// s 1
//[10 1 115 16 1]
// s 2
//[10 1 115 16 2]
// s 3
// [10 1 115 16 3]
// a 3
// [10 1 97 16 3]
// z 3
// [10 1 122 16 3]
// z 300
// [10 1 122 16 172 2]
// z 255
// [10 1 122 16 255 1]
// z 256
// [10 1 122 16 128 2]
// z 257
// [10 1 122 16 129 2]
// z 258
// [10 1 122 16 130 2]
// z 259
// [10 1 122 16 131 2]
// z 127
// [10 1 122 16 127]
// z 128
// [10 1 122 16 128 1]
// z 129
// [10 1 122 16 129 1]
// z 131
// [10 1 122 16 131 1]
/**
z 512
10 1 122 16 128 4
z 511
[10 1 122 16 255 3]
z 5110
[10 1 122 16 246 39]
zz 5100
[10 2 122 122 16 246 39]
*/
// tag << 3| wiretype
/**
10 = 1<<3 | string type = 8|2 = 10
1 = 字符串長度
字符串的ascii碼
16 = 2<<3 | varint type = 16|0 = 16
127
0111 1111
128 = 2^8
1000000
低7位+前綴1 + 低0位 + 剩余位
= (1)000000 + (0)0000001
511
=1 1111 1111
1+低七位紊选,補(bǔ)0+高2位
1 111111 000000 11
255 3
5110
0001 0011 1111 0110
100111 1110110
11110110 0100111
246 39
*/
name := "z"
age := int32(150)
person := &Person{
Name: &name,
Age: &age,
}
fmt.Println("person : ", person)
fname := "address.dat"
// 將person進(jìn)行序列化
out, err := proto.Marshal(person)
fmt.Println(out)
if err != nil {
fmt.Println("Failed to encode address person:", err)
}
// 將序列化的內(nèi)容寫入文件
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
fmt.Println("Failed to write address person:", err)
}
// 讀取寫入的二進(jìn)制數(shù)據(jù)
in, err := ioutil.ReadFile(fname)
if err != nil {
fmt.Println("Error reading file:", err)
}
// 定義一個空的結(jié)構(gòu)體
person2 := &Person{}
// 將從文件中讀取的二進(jìn)制進(jìn)行反序列化
if err := proto.Unmarshal(in, person2); err != nil {
fmt.Println("Failed to parse address person:", err)
}
fmt.Println("person2: ", person2)
}
func testPerson1() {
name := "a"
age := int32(1)
person := &Person{
Name: &name,
Age: &age,
}
fmt.Println("person : ", person)
fname := "address.dat"
// 將person進(jìn)行序列化
out, err := proto.Marshal(person)
fmt.Println(out)
if err != nil {
fmt.Println("Failed to encode address person:", err)
}
// 將序列化的內(nèi)容寫入文件
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
fmt.Println("Failed to write address person:", err)
}
// 讀取寫入的二進(jìn)制數(shù)據(jù)
in, err := ioutil.ReadFile(fname)
if err != nil {
fmt.Println("Error reading file:", err)
}
// 定義一個空的結(jié)構(gòu)體
person2 := &Person1{}
// 將從文件中讀取的二進(jìn)制進(jìn)行反序列化
if err := proto.Unmarshal(in, person2); err != nil {
fmt.Println("Failed to parse address person:", err)
}
fmt.Println("person2: ", person2)
}
proto語法
語法規(guī)則詳見 https://developers.google.com/protocol-buffers/docs/proto
這里作出部分說明
在 proto 中啼止,所有結(jié)構(gòu)化的數(shù)據(jù)都被稱為 message。
如果開頭第一行不聲明 ?syntax = "proto3";?兵罢,則默認(rèn)使用 proto2 進(jìn)行解析献烦。
分配字段編號
每個消息定義中的每個字段都有**唯一的編號**。這些字段編號用于標(biāo)識消息二進(jìn)制格式中的字段卖词,并且在使用消息類型后不應(yīng)更改巩那。請注意,范圍 1 到 15 中的字段編號需要一個字節(jié)進(jìn)行編碼此蜈,包括字段編號和字段類型即横。范圍 16 至 2047 中的字段編號需要兩個字節(jié)。所以你應(yīng)該保留數(shù)字 1 到 15 作為非常頻繁出現(xiàn)的消息元素裆赵。請記住為將來可能添加的頻繁出現(xiàn)的元素留出一些空間东囚。
字段規(guī)則
repeated 0-n
optional 0-1
required 1
保留字段
如果您通過完全刪除某個字段或?qū)⑵渥⑨尩魜砀孪㈩愋停敲次磥淼挠脩艨梢栽趯υ擃愋瓦M(jìn)行自己的更新時重新使用該字段號战授。如果稍后加載到了的舊版本 .proto 文件页藻,則會導(dǎo)致服務(wù)器出現(xiàn)嚴(yán)重問題,例如數(shù)據(jù)混亂陈醒,隱私錯誤等等惕橙。確保這種情況不會發(fā)生的一種方法是指定刪除字段的字段編號(或名稱,這也可能會導(dǎo)致 JSON 序列化問題)為 reserved钉跷。如果將來的任何用戶試圖使用這些字段標(biāo)識符弥鹦,Protocol Buffers 編譯器將會報錯。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
各個語言標(biāo)量類型對應(yīng)關(guān)系
https://developers.google.com/protocol-buffers/docs/proto3#scalar
枚舉,默認(rèn)值彬坏,嵌套定義,MAP,定義service朦促,
不展開
更新一個message
如果后面發(fā)現(xiàn)之前定義 message 需要增加字段了,這個時候就體現(xiàn)出 Protocol Buffer 的優(yōu)勢了栓始,不需要改動之前的代碼务冕。不過需要滿足以下 10 條規(guī)則
不要改動原有字段的數(shù)據(jù)結(jié)構(gòu)。
如果您添加新字段幻赚,新字段應(yīng)該用optional或者repeated禀忆,則任何由代碼使用“舊”消息格式序列化的消息仍然可以通過新生成的代碼進(jìn)行分析。您應(yīng)該記住這些元素的默認(rèn)值落恼,以便新代碼可以正確地與舊代碼生成的消息進(jìn)行交互箩退。同樣,由新代碼創(chuàng)建的消息可以由舊代碼解析:舊的二進(jìn)制文件在解析時會簡單地忽略新字段佳谦。
只要字段號在更新的消息類型中不再使用戴涝,字段可以被刪除。您可能需要重命名該字段钻蔑,可能會添加前綴“OBSOLETE_”啥刻,或者標(biāo)記成保留字段號 reserved,以便將來的 .proto 用戶不會意外重復(fù)使用該號碼咪笑。
int32可帽,uint32,int64蒲肋,uint64 和 bool 全都兼容蘑拯。這意味著您可以將字段從這些類型之一更改為另一個字段而不破壞向前或向后兼容性。如果一個數(shù)字從不適合相應(yīng)類型的線路中解析出來兜粘,則會得到與在 C++ 中將該數(shù)字轉(zhuǎn)換為該類型相同的效果(例如申窘,如果將 64 位數(shù)字讀為 int32,它將被截斷為 32 位)孔轴。
sint32 和 sint64 相互兼容剃法,但與其他整數(shù)類型不兼容。
只要字節(jié)是有效的UTF-8路鹰,string 和 bytes 是兼容的贷洲。
嵌入式 message 與 bytes 兼容,如果 bytes 包含 message 的 encoded version晋柱。
fixed32與sfixed32兼容优构,而fixed64與sfixed64兼容。
enum 就數(shù)組而言雁竞,是可以與 int32钦椭,uint32拧额,int64 和 uint64 兼容(請注意,如果它們不適合彪腔,值將被截斷)侥锦。但是請注意,當(dāng)消息反序列化時德挣,客戶端代碼可能會以不同的方式對待它們:例如恭垦,未識別的 proto3 枚舉類型將保留在消息中,但消息反序列化時如何表示是與語言相關(guān)的格嗅。(這點和語言相關(guān)番挺,上面提到過了)Int 域始終只保留它們的值。
將單個值更改為新的成員是安全和二進(jìn)制兼容的吗浩。如果您確定一次沒有代碼設(shè)置多個字段建芙,則將多個字段移至新的字段可能是安全的。將任何字段移到現(xiàn)有字段中都是不安全的懂扼。(注意字段和值的區(qū)別,字段是 field右蒲,值是 value)
未知字段
未知數(shù)字段是 protocol buffers 序列化的數(shù)據(jù)阀湿,表示解析器無法識別的字段。例如瑰妄,當(dāng)一個舊的二進(jìn)制文件解析由新的二進(jìn)制文件發(fā)送的新數(shù)據(jù)的數(shù)據(jù)時陷嘴,這些新的字段將成為舊的二進(jìn)制文件中的未知字段。
Proto3 實現(xiàn)可以成功解析未知字段的消息间坐,但是灾挨,實現(xiàn)可能會或可能不會支持保留這些未知字段。你不應(yīng)該依賴保存或刪除未知域竹宋。對于大多數(shù) Google protocol buffers 實現(xiàn)劳澄,未知字段在 proto3 中無法通過相應(yīng)的 proto 運行時訪問,并且在反序列化時被丟棄和遺忘蜈七。這是與 proto2 的不同行為秒拔,其中未知字段總是與消息一起保存并序列化。
編碼原理
https://developers.google.com/protocol-buffers/docs/encoding
Base 128 Varints 編碼
Varint 是一種緊湊的表示數(shù)字的方法飒硅。它用一個或多個字節(jié)來表示一個數(shù)字砂缩,值越小的數(shù)字使用越少的字節(jié)數(shù)。這能減少用來表示數(shù)字的字節(jié)數(shù)三娩。
Varint 中的每個字節(jié)(最后一個字節(jié)除外)都設(shè)置了最高有效位(msb)庵芭,這一位表示還會有更多字節(jié)出現(xiàn)。每個字節(jié)的低 7 位用于以 7 位組的形式存儲數(shù)字的二進(jìn)制補(bǔ)碼表示
如果用不到 1 個字節(jié)雀监,那么最高有效位設(shè)為 0 双吆,如下面這個例子,
1 用一個字節(jié)就可以表示,所以 msb 為 0.
0000 0001
如果需要多個字節(jié)表示伊诵,msb 就應(yīng)該設(shè)置為 1 单绑。
例如 300,如果用 Varint 表示的話:
二進(jìn)制
0000 0001 0010 1100 =>
10 0101100 => 逆序曹宴,加msb
(1)0101100 (000000)10
即172 2
例如5110
*二進(jìn)制* *0001**0011**1111**0110* *=>*
0100111 1110110 =》 逆序搂橙,加msb
11110110 00100111 =>
*結(jié)果* *246**39*
本來一個int32,要占4個字節(jié)長度的數(shù)字笛坦,這里只占了2個字節(jié)(246占8位 39占8位)
那 Varint 是怎么編碼的呢区转?
下面代碼是 Varint int 32 的編碼計算方法。
解碼是可逆的過程
1.如果是多個字節(jié)版扩,先去掉每個字節(jié)的 msb(通過邏輯或運算)废离,每個字節(jié)只留下 7 位。
2.每7位給拼接起來
varint一定更壓縮嗎
讀到這里可能有讀者會問了礁芦,Varint 不是為了緊湊 int 的么蜻韭?那 300 本來可以用 2 個字節(jié)表示,現(xiàn)在還是 2 個字節(jié)了柿扣,哪里緊湊了肖方,花費的空間沒有變啊未状?俯画!
Varint 確實是一種緊湊的表示數(shù)字的方法。它用一個或多個字節(jié)來表示一個數(shù)字司草,值越小的數(shù)字使用越少的字節(jié)數(shù)艰垂。這能減少用來表示數(shù)字的字節(jié)數(shù)。比如對于 int32 類型的數(shù)字埋虹,一般需要 4 個 byte 來表示猜憎。但是采用 Varint,對于很小的 int32 類型的數(shù)字吨岭,則可以用 1 個 byte 來表示拉宗。當(dāng)然凡事都有好的也有不好的一面,采用 Varint 表示法辣辫,大的數(shù)字則需要 5 個 byte 來表示旦事。從統(tǒng)計的角度來說,一般不會所有的消息中的數(shù)字都是大數(shù)急灭,因此大多數(shù)情況下姐浮,采用 Varint 后,可以用更少的字節(jié)數(shù)來表示數(shù)字信息葬馋。
300 如果用 int32 表示卖鲤,需要 4 個字節(jié)肾扰,現(xiàn)在用 Varint 表示,只需要 2 個字節(jié)了蛋逾〖恚縮小了一半!
Message Structure 編碼
protocol buffer 中 message 是一系列鍵值對区匣。message 的二進(jìn)制版本只是使用字段號(field's number 和 wire_type)作為 key偷拔。每個字段的名稱和聲明類型只能在解碼端通過引用消息類型的定義(即 ?.proto? 文件)來確定。這一點也是人們常常說的 protocol buffer 比 JSON亏钩,XML 安全一點的原因莲绰,如果沒有數(shù)據(jù)結(jié)構(gòu)描述 ?.proto? 文件,拿到數(shù)據(jù)以后是無法解釋成正常的數(shù)據(jù)的姑丑。
當(dāng)消息編碼時蛤签,鍵和值被連接成一個字節(jié)流。當(dāng)消息被解碼時栅哀,解析器需要能夠跳過它無法識別的字段震肮。這樣,可以將新字段添加到消息中昌屉,而不會破壞不知道它們的舊程序钙蒙。這就是所謂的 “向后”兼容性。
為此间驮,線性的格式消息中每對的“key”實際上是兩個值,其中一個是來自?.proto?文件的字段編號马昨,加上提供正好足夠的信息來查找下一個值的長度竞帽。在大多數(shù)語言實現(xiàn)中,這個 key 被稱為 tag鸿捧。
key 的計算方法
即?(field_number << 3) | wire_type?屹篓,換句話說,key 的最后 3 位表示的就是 ?wire_type?匙奴。
假設(shè)遇到
required int32 a = 1;
對應(yīng)key計算值即為
000 1000
即 1(field_num)<<3| 0 (varint)
Signed Integers 編碼
Protobuf中采用Zigzag堆巧,正負(fù)數(shù)交叉的方式來表示有符號數(shù)。
我們知道泼菌,計算機(jī)中采用補(bǔ)碼的方式來表示有符號數(shù)谍肤,其最高位是符號位,正數(shù)的補(bǔ)碼等于原碼哗伯,負(fù)數(shù)的補(bǔ)碼等于原碼符號位不變荒揣,其余位取反再加1。所以如果我們使用int32表示1和-1焊刹,則-1需要表示成很大的正數(shù)系任,需要5個字節(jié):
1(10進(jìn)制) = 00000000_00000000_00000000_00000001(補(bǔ)碼)
= 00000001(Varint int32)
-1(10進(jìn)制) = 11111111_11111111_11111111_11111111(補(bǔ)碼)
= 11111111_11111111_11111111_11111111_00001111(假如用Varint表示)
我們注意到絕對值很小的正負(fù)數(shù)其前面都是0或者1恳蹲,如果我們能將重復(fù)的位壓縮,則可以很大的節(jié)省空間俩滥。對于正數(shù)比較好處理嘉蕾,把前面無意義的0去掉就行。對于負(fù)數(shù)霜旧,則可以先把符號位移到最低位错忱,再對數(shù)據(jù)位求反,就可以把前面的0都壓縮了颁糟。-1和1的變換過程如下:
Non-varint Numbers
Non-varint 數(shù)字比較簡單航背,double 、fixed64 的 wire_type 為 1棱貌,在解析時告訴解析器玖媚,該類型的數(shù)據(jù)需要一個 64 位大小的數(shù)據(jù)塊即可。同理婚脱,float 和 fixed32 的 wire_type 為5今魔,給其 32 位數(shù)據(jù)塊即可。兩種情況下障贸,都是高位在后错森,低位在前。
說 Protocol Buffer 壓縮數(shù)據(jù)沒有到極限篮洁,原因就在這里涩维,因為并沒有壓縮 float、double 這些浮點類型袁波。
字符串
字符串對應(yīng)wire_type=2瓦阐,是一種指定長度的編碼方式:key + length + content,key 的編碼方式是統(tǒng)一的篷牌,length 采用 varints 編碼方式睡蟋,content 就是由 length 指定長度的 Bytes。
message Test2 {
optional string b = 2;
}
設(shè)置該值為"testing"枷颊,二進(jìn)制格式查看:
12 07 74 65 73 74 69 6e 67
12(16進(jìn)制) = 18(10進(jìn)制) = 2(field_num)<<3 | 2(wire_type)
07(16進(jìn)制) = 7(10進(jìn)制), testing長度為7
74(16) = 120(10) = 't' (比如 'a'=97)
性能:
參考referhttps://www.infoq.cn/article/json-is-5-times-faster-than-protobuf里面最后的圖
總結(jié)戳杀,重點:
總結(jié)
Protocol Buffer 利用 varint 原理壓縮數(shù)據(jù)以后,二進(jìn)制數(shù)據(jù)非常緊湊夭苗,option 也算是壓縮體積的一個舉措信卡。所以 pb 體積更小,如果選用它作為網(wǎng)絡(luò)數(shù)據(jù)傳輸听诸,勢必相同數(shù)據(jù)坐求,消耗的網(wǎng)絡(luò)流量更少。但是并沒有壓縮到極限晌梨,float桥嗤、double 浮點型都沒有壓縮须妻。
Protocol Buffer 比 JSON 和 XML 少了 {、}泛领、: 這些符號荒吏,體積也減少一些。
Protocol Buffer 另外一個核心價值在于提供了一套工具渊鞋,一個編譯工具绰更,自動化生成 get/set 代碼。簡化了多語言交互的復(fù)雜度锡宋,使得編碼解碼工作有了生產(chǎn)力儡湾。
Protocol Buffer 不是自我描述的,離開了數(shù)據(jù)描述 .proto 文件执俩,就無法理解二進(jìn)制數(shù)據(jù)流徐钠。這點即是優(yōu)點,使數(shù)據(jù)具有一定的“加密性”役首,也是缺點尝丐,數(shù)據(jù)可讀性極差。所以 Protocol Buffer 非常適合內(nèi)部服務(wù)之間 RPC 調(diào)用和傳遞數(shù)據(jù)。
Protocol Buffer 具有向后兼容的特性,更新數(shù)據(jù)結(jié)構(gòu)以后毅否,老版本依舊可以兼容,這也是 Protocol Buffer 誕生之初被寄予解決的問題失息。因為編譯器對不識別的新增字段會跳過不處理。
重點和思考
總結(jié)
Protocol Buffer 利用 varint 原理壓縮數(shù)據(jù)以后档址,二進(jìn)制數(shù)據(jù)非常緊湊根时,option 也算是壓縮體積的一個舉措。所以 pb 體積更小辰晕,如果選用它作為網(wǎng)絡(luò)數(shù)據(jù)傳輸,勢必相同數(shù)據(jù)确虱,消耗的網(wǎng)絡(luò)流量更少含友。但是并沒有壓縮到極限,float校辩、double 浮點型都沒有壓縮窘问。
Protocol Buffer 比 JSON 和 XML 少了 {、}宜咒、: 這些符號惠赫,體積也減少一些。
Protocol Buffer 另外一個核心價值在于提供了一套工具故黑,一個編譯工具儿咱,自動化生成 get/set 代碼庭砍。簡化了多語言交互的復(fù)雜度,使得編碼解碼工作有了生產(chǎn)力混埠。
Protocol Buffer 不是自我描述的怠缸,離開了數(shù)據(jù)描述 .proto 文件,就無法理解二進(jìn)制數(shù)據(jù)流钳宪。這點即是優(yōu)點揭北,使數(shù)據(jù)具有一定的“加密性”,也是缺點吏颖,數(shù)據(jù)可讀性極差搔体。所以 Protocol Buffer 非常適合內(nèi)部服務(wù)之間 RPC 調(diào)用和傳遞數(shù)據(jù)。
Protocol Buffer 具有向后兼容的特性半醉,更新數(shù)據(jù)結(jié)構(gòu)以后疚俱,老版本依舊可以兼容,這也是 Protocol Buffer 誕生之初被寄予解決的問題奉呛。因為編譯器對不識別的新增字段會跳過不處理计螺。
重點和思考
編解碼:
varint編解碼算法,以及優(yōu)劣壓縮是否到極致
tag計算方式瞧壮,字段名的意義作用時機(jī)
zigzag處理有符號數(shù)
Tag - Length - Value處理string等
如何評價一個編解碼算法登馒,協(xié)議:
編解碼時間,針對不同類型的支持
壓縮空間
前后兼容性:
向后兼容性的保證:不認(rèn)識的字段跳過
改動咆槽,加字段陈轿,刪字段,字段改名字
可讀性,安全性,自解釋性
跨語言
生態(tài)和工具支持:
compiler和runtime
compiler類似代碼生成器秦忿,根據(jù)proto生成代碼
對比公司現(xiàn)有工具
runtime即序列化和反序列化
對比公司現(xiàn)有工具
思考題:
1.proto里面的字段名有什么意義麦射,對于生成pb文件有什么作用
沒用,client灯谣,server同類型同field_num哪怕字段名不一樣潜秋,也可以順利解析
` message Person {`
`required string name = 1;`
`required int32 age = 2;`
` }`
` message Person1 {`
` required string name = 1;`
` optional int32 age1 = 2;`
` }`
可以用Person{"zxc",-2}去序列化生成二進(jìn)制文件,再用Person1的proto反序列化胎许,得到name="zxc",age1=-2
2.repeated,optional這些在proto中是如何生效的
序列化和反序列化的時候會檢查峻呛,但是并不會體現(xiàn)在二進(jìn)制文件中,也就是同樣一個proto辜窑,把required改成proto钩述,進(jìn)行一樣的賦值,生成的二進(jìn)制不會有任何差別
` message Person {`
`required string name = 1;`
`required int32 age = 2;`
` }`
`Person("a","1")序列化后是[10 1 97 16 1]`
`message Person1 { `
`optional string name = 1;`
`optional int32 age = 2;`
`}`
同樣的Person1("a","1")序列化后也是[10 1 97 16 1]
refer
http://www.reibang.com/p/c1723e5f6a46 安裝配置和demo
https://developers.google.com/protocol-buffers 官網(wǎng)
https://halfrost.com/protobuf_encode/ https://halfrost.com/protobuf_decode/ 比較好的材料,官網(wǎng)翻譯版 mainly refered
https://zhuanlan.zhihu.com/p/73549334 簡單描述
https://izualzhy.cn/protobuf-encode-varint-and-zigzag 搞圖
測評
https://www.infoq.cn/article/json-is-5-times-faster-than-protobuf