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