
作為一個(gè)程序猿,對(duì)造輪子這事情可以說(shuō)是情有獨(dú)鐘,幾乎程序猿內(nèi)心都存在一個(gè)夢(mèng)想是去將開(kāi)源的技術(shù)都實(shí)現(xiàn)一遍,所有從本篇開(kāi)始,我會(huì)開(kāi)一個(gè)造輪子系列。
前言
首先,看看這個(gè),想必大家對(duì)下面這種簡(jiǎn)歷看得比較多了吧?
精通JAVA,Python,熟練掌握C++
精通Redis,Memcached,Mysql
精通Nginx配置,模塊開(kāi)發(fā)
精通Kafka,ActiveMQ 等消息隊(duì)列
精通常用數(shù)據(jù)結(jié)構(gòu)和算法
精通網(wǎng)絡(luò)編程,多線程編程技術(shù),高性能服務(wù)器技術(shù)
精通tcp/ip協(xié)議棧,熟悉內(nèi)核網(wǎng)絡(luò)子系統(tǒng)代碼
精通nginx代碼及模塊開(kāi)發(fā)
上面每一條都涉及好多輪子,每一個(gè)都是精通,如果真能做到。那這個(gè)人可以說(shuō)是碼農(nóng)中的戰(zhàn)斗機(jī)。
那我們現(xiàn)在目標(biāo)就是去做這個(gè)戰(zhàn)斗機(jī)。而這個(gè)方法,就是自己去造輪子,造的目的不是為了在項(xiàng)目中使用自己造的輪子,而是為了去了解輪子的構(gòu)造,然后自己動(dòng)手去體會(huì)造輪子的過(guò)程。
后端的輪子們
說(shuō)起后端的輪子們,大家都可以說(shuō)出一大串來(lái),我們大致來(lái)數(shù)一數(shù)啊。
抗在最前面的:LVS,F(xiàn)5,HAProxy這類負(fù)載均衡
接下來(lái)有Nginx,Apache,Lighttpd這類Http服務(wù)
http服務(wù)后則是各種容器,部署著我們的業(yè)務(wù)邏輯
存儲(chǔ)這邊有Redis,Memcached這一類KV存儲(chǔ)器和緩存系統(tǒng)
如果是多機(jī)部署,肯定還有Kafka,ActiveMQ這種負(fù)責(zé)解耦的消息隊(duì)列
為了實(shí)現(xiàn)集群通信,肯定少不了Thrift這種RPC框架和Protobuf這種序列化技術(shù)
再高端點(diǎn),到了分布式領(lǐng)域了,就是更多的輪子了。。zookeeper、raft等等
還有大數(shù)據(jù)系列hadoop。spark。。。。。
本文先開(kāi)始我們的第一個(gè)輪子,服務(wù)器通信需要用的數(shù)據(jù)序列化反序列技術(shù):protobuf。
基礎(chǔ)輪子:protobuf
講基礎(chǔ)前,先附上一張極客時(shí)間中的一個(gè)技術(shù)需要從哪些角度來(lái)講的圖片,本文也會(huì)盡可能從這些個(gè)方面來(lái)講。
應(yīng)用角度
1. 問(wèn)題:”干什么用“
2. 技術(shù)規(guī)范:”怎么用“
3. 最佳實(shí)踐:”怎么能用好“
4. 市場(chǎng)應(yīng)用趨勢(shì):“誰(shuí)用,用在哪”
設(shè)計(jì)角度
1. 目標(biāo):“做到什么”
2. 實(shí)現(xiàn)原理:“怎么做到”
3. 優(yōu)劣局限:“做得怎么樣”
4. 演進(jìn)趨勢(shì):“未來(lái)如何”
正文
Protocol buffers
從應(yīng)用角度看protobuf是干什么用的?
序列化數(shù)據(jù)用的?什么時(shí)候需要序列化?當(dāng)數(shù)據(jù)需要存儲(chǔ)或者網(wǎng)絡(luò)傳輸?shù)臅r(shí)候。為什么呢?
在存儲(chǔ)或者傳輸?shù)臅r(shí)候,我們能看到都是一些二進(jìn)制數(shù)據(jù),即010101……的bit。
假設(shè)我們看的一個(gè)對(duì)象是:
Struct myData {
Int a;
Int b;
}
data = myData {
a:1,
b:2,
}
那我們?cè)诰W(wǎng)絡(luò)上收到是一個(gè)字節(jié)流,我們?yōu)榱四軌驈淖止?jié)流中恢復(fù)出數(shù)據(jù) data,我們要做的工作是:
正確識(shí)別出data在字節(jié)流中開(kāi)始和結(jié)束的位置
識(shí)別出a的值,識(shí)別出b的值
一個(gè)可能的字節(jié)流協(xié)議就是:
剛開(kāi)始是8bit標(biāo)明后續(xù)數(shù)據(jù)是哪個(gè)結(jié)構(gòu),然后是兩個(gè)4字節(jié)表示a和b。
注意?。。。?!上面做出上面這個(gè)假設(shè),有幾點(diǎn)是我們默認(rèn)的:
我們認(rèn)為字節(jié)流開(kāi)始先是8bit標(biāo)明是哪個(gè)數(shù)據(jù)結(jié)構(gòu),此處是myData(ps:不同結(jié)構(gòu)之間編號(hào)不同)
最多能夠支持2^8種結(jié)構(gòu)
通訊雙方都需要拿到myData的定義文件
以下是一個(gè)上面實(shí)現(xiàn)的示例代碼:
可以看到在go中很容易就實(shí)現(xiàn)了我們的一個(gè)數(shù)據(jù)結(jié)構(gòu)的序列化反序列化。
設(shè)計(jì)角度,做到什么?
上面我們只是實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的實(shí)現(xiàn)了一個(gè)序列化方法,下面我們來(lái)看如果要實(shí)現(xiàn)一個(gè)生產(chǎn)環(huán)境中的序列化協(xié)議,需要做到哪幾點(diǎn)。
通用性:語(yǔ)言、平臺(tái)無(wú)關(guān)
高性能:序列化和反序列化都要快
高壓縮:序列化后數(shù)據(jù)盡可能小,小就意味著網(wǎng)絡(luò)傳輸數(shù)據(jù)少
兼容性:數(shù)據(jù)結(jié)構(gòu)改變了,也能夠支持新老版本
下面我們帶著這些目標(biāo)來(lái)從應(yīng)用角度來(lái)看下”怎么用“
這個(gè)就是官方文檔了https://developers.google.com/protocol-buffers/docs/gotutorial,里面有詳細(xì)的說(shuō)明,另外我自己給了一份使用示例:https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/protobuf/v2
講完使用下面就是設(shè)計(jì)角度:如何做到的?
首先我們看前文我們自己實(shí)現(xiàn)的簡(jiǎn)易序列化、反序列方法,我們對(duì)每個(gè)結(jié)構(gòu)進(jìn)行編碼,然后在頭部寫(xiě)上該結(jié)構(gòu)是啥,然后后面就是結(jié)構(gòu)中每個(gè)字段的具體值,接著我們寫(xiě)了序列化器的目標(biāo)是:簡(jiǎn)易、高效、兼容,下面我們從這幾個(gè)方面來(lái)看protobuf有什么改進(jìn)的地方。
先來(lái)個(gè)小插曲,protobuf全稱是Protocol buffers,其中buffers點(diǎn)名了使用上非常重要的一個(gè)點(diǎn),即我們?cè)诜葱蛄谢囊欢味M(jìn)制數(shù)據(jù)的時(shí)候,我們要將其先讀入到buffer中,然后再識(shí)別出單個(gè)數(shù)據(jù)結(jié)構(gòu)的開(kāi)頭和結(jié)尾,最后才能正確的反序列化出來(lái)。
前面我們?cè)O(shè)計(jì)的時(shí)候,還在頭部對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行了編碼,那為了能夠做到更高效,數(shù)據(jù)更小,我們是否可以把這個(gè)頭也去掉呢?
當(dāng)然是可以的,于是我們的結(jié)構(gòu)就變?yōu)榱酥挥袑?duì)應(yīng)的字段值了,這么做的一個(gè)前提是:!!我們必須清楚知道我們識(shí)別出來(lái)二進(jìn)制數(shù)據(jù),其對(duì)應(yīng)的具體是哪個(gè)數(shù)據(jù)結(jié)構(gòu)。?。?/p>
現(xiàn)在我們?nèi)サ袅私Y(jié)構(gòu)描述,那怎么能夠做到更小呢?譬如同樣是int64,1 和 1<<32沒(méi)必要都用8字節(jié)來(lái)表示,譬如我們可以先對(duì)數(shù)據(jù)類型做一個(gè)編碼,然后緊跟著后續(xù)使用的bit,再跟著真正的數(shù)據(jù)。
每個(gè)部分分別用幾個(gè)bit來(lái)表示呢?
數(shù)據(jù)類型:根據(jù)支持的類型進(jìn)行編碼,如果總共能支持16種類型,那就是4bit
后續(xù)有效字節(jié):這個(gè)比較難辦,由于我們不確定數(shù)據(jù)大小,我們就無(wú)法固定bit來(lái)表示。
那一個(gè)解決方法就是:我們?nèi)サ粲行ё止?jié)的字段,我們把這個(gè)是否有更多數(shù)據(jù)信息編碼進(jìn)數(shù)據(jù)本身中,示意圖如下:
解決了編碼int類型的字段后,如果遇到string類型呢?這種類型首先也是數(shù)據(jù)類型描述,接著應(yīng)該要是編碼后續(xù)有效字節(jié),這是一個(gè)int,這可以采用上面的方法來(lái)編碼,再跟著就是有效數(shù)據(jù)了。
以上就是protobuf在編碼數(shù)據(jù)時(shí)采用編碼方式的主要思想,具體可以看https://developers.google.com/protocol-buffers/docs/encoding。
目前protobuf支持的數(shù)據(jù)類型
上面有個(gè)主意的對(duì)于有符號(hào)數(shù),我們要單獨(dú)處理下,因?yàn)橛蟹?hào)數(shù)的最高位是通過(guò)0,1來(lái)表示正負(fù)的,但是上面編碼中最高位卻用來(lái)表示是否有后續(xù)數(shù)據(jù)了,所以我們要通過(guò)ZigZag 編碼將有符號(hào)轉(zhuǎn)換為無(wú)符號(hào)。
原理很簡(jiǎn)單,就是通過(guò)下面的編碼方式:
解決了編碼后,我們來(lái)看最后一個(gè)問(wèn)題:兼容性。
如果我們改變了數(shù)據(jù)結(jié)構(gòu):新增或者刪除了字段怎么辦???
這個(gè)也好解決,那我們就給所有字段加上編號(hào),通過(guò)字段來(lái)表示這個(gè)數(shù)據(jù)是結(jié)構(gòu)體中哪個(gè)字段,protobuf在設(shè)計(jì)上編碼方式如下:
圖片來(lái)自:高效的數(shù)據(jù)壓縮編碼方式 Protobuf
從上圖中tag的編碼,我們可以發(fā)現(xiàn),如果field_num > 16的話,tag編碼出來(lái)會(huì)使用超過(guò)1字節(jié),所有對(duì)于我們經(jīng)常使用的字段,建議將其編碼到0-15,減少tag位數(shù)。
實(shí)現(xiàn)原理小結(jié)
下面我們對(duì)上面介紹的實(shí)現(xiàn)原理做個(gè)小結(jié)
高效:變長(zhǎng)編碼,非自描述
兼容:對(duì)filed進(jìn)行編碼
下面我們?cè)購(gòu)膽?yīng)用角度講下 protobuf 的最佳實(shí)踐和市場(chǎng)應(yīng)用趨勢(shì)。
protobuf剛開(kāi)始設(shè)計(jì)出來(lái)主要是為了解決接口兼容性問(wèn)題,目前是主要用在內(nèi)部服務(wù)之間RPC調(diào)用和傳遞數(shù)據(jù),目前時(shí)候用protobuf作為序列化的rpc框架有g(shù)RPC,這也是后續(xù)我們會(huì)介紹的。
最后我們從設(shè)計(jì)角度來(lái)看下protobuf的優(yōu)劣局限和演進(jìn)趨勢(shì)。
優(yōu)點(diǎn)
protobuf最大的優(yōu)點(diǎn)就是前后兼容性,已經(jīng)部署的使用老數(shù)據(jù)格式的服務(wù),即使接口升級(jí)了也可以繼續(xù)使用,然后就是性能,當(dāng)然是快了,具體可以看 序列化 / 反序列化性能
缺點(diǎn)
相比較json來(lái)說(shuō),可讀性差,特別是在調(diào)試階段,相比較json我們無(wú)法清晰的知道輸入和輸出。
最后
總結(jié)下本文
Protobuf設(shè)計(jì)之初主要是為了解決兼容性問(wèn)題,實(shí)現(xiàn)上是對(duì)每個(gè)字段進(jìn)行編號(hào),當(dāng)遇到不存在的字段時(shí),則忽略掉。
Protobuf為了能夠做到高性能,在編碼時(shí)采用了Tag - Value (Tag - Length - Value)的方式,使序列化后的數(shù)據(jù)更緊湊
Protobuf為了能夠做到高性能,丟棄了自描述信息,即我們只拿到數(shù)據(jù),而沒(méi)有拿到proto文件,我們是無(wú)法反序列數(shù)據(jù)的
Protobuf提供了一套編譯工具,能夠生成不同語(yǔ)言的數(shù)據(jù)序列化、反序列化方法,極大的提高了易用性
預(yù)告
下一篇我們會(huì)介紹grpc,來(lái)看下rpc框架中是怎么使用protobuf的。
參考
高效的數(shù)據(jù)壓縮編碼方式 Protobuf
官方文檔