Protobuf原理分析

一泉沾、什么是Protobuf

Protobuf(Google Protocol Buffers)是Google提供一個具有高效的協(xié)議數(shù)據(jù)交換格式工具庫侥衬,類似于常用的XML及JSON梯投,但具有更小的傳輸體積钦扭、更高的編碼、解碼能力汇鞭,特別適合于數(shù)據(jù)存儲凄敢、網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)葘Υ鎯w積碌冶、實時性要求高的領(lǐng)域。

二涝缝、Protobuf真的能壓縮空間嗎扑庞?

String.png

Protobuf String.png

經(jīng)過對比我們發(fā)現(xiàn)使用了Protobuf的的數(shù)據(jù)比沒有使用Protobuf編碼的數(shù)據(jù)長度還多了兩個字節(jié),這到底是為什么拒逮?我們接著往下看一個Long類型的對比


Long.png

Protobuf Long.png

比如Long類型發(fā)現(xiàn)罐氨,沒有Protobuf編碼的Long類型的數(shù)據(jù)占了8個字節(jié),而使用Protobuf的只占了數(shù)據(jù)只占了四個字節(jié)滩援,這是為什么栅隐?接下來我們一起來分析下原因

三、Protobuf編碼結(jié)構(gòu)

image.png
  • Tag
    field_number: message 定義字段時指定的字段編號
    wire_type: ProtoBuf 編碼類型玩徊,根據(jù)這個類型選擇不同的 Value 編碼方案租悄。

  • Length 是可選的,不同類型的數(shù)據(jù)編碼結(jié)構(gòu)可能會變成 Tag - Value 的格式恩袱。沒有Length如何確認Value的邊界泣棋?答案就是 Varint 編碼。

  • Value 數(shù)據(jù)內(nèi)容根據(jù)數(shù)據(jù)類型不同使用不同方式的編碼的數(shù)據(jù)

1畔塔、Varint這個類型編碼方式會對針對sint 32和sint64做一個特殊處理外傅,先用ZigTag編碼處理再進行Varint編碼纪吮。
2、Start group 和 End group 兩種類型已被廢棄萎胰。

四、Tag編解碼

#Login.proto
message LoginRequest{
        string username = 1;
        string password = 2;
}

Tag編碼

Tag= (field_number << 3 ) | wire_type
username 屬性的tag編碼
usernameTag = 1 << 3 | 2
usernameTag = 00001010

Tag解碼

wire_type = Tag & 3
wire_type = 00001010 & 3 = 010
field_number = Tag >> 3
field_number = 00001010 >> 3 = 00001

Wire Type 的類型如下表所示

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

五棚辽、Varints 編解碼

Varints 編碼的規(guī)則主要特點:
在每個字節(jié)第一個 bit 設(shè)置了 msb(most significant bit )技竟,標識是否需要繼續(xù)讀取下一個字節(jié)
存儲數(shù)值對應(yīng)的二進制補碼
補碼的低位排在前面

  • 編碼

對388888的補碼進行編碼
00000000 00000000 00000000 00000000 00000000 00000101 11101111 00011000
1、去掉高位多余的0
101 11101111 00011000
2屈藐、從后依次向前取 7 位組并反轉(zhuǎn)排序
0011000 1011110 0010111
3榔组、加上 msb
1#0011000 1#1011110 0#0010111

  • 解碼

10011000 11011110 00010111 …
每個字節(jié)的第一個 bit 為 msb 位,msb = 1 表示需要再讀一個字節(jié)(還未結(jié)束)联逻,msb = 0 表示無需再讀字節(jié)(讀取到此為止)搓扯。
1、讀取Value的值
10011000 11011110 00010111
2包归、去掉msb
0011000 1011110 0010111
3锨推、將這三個 7-bit 組反轉(zhuǎn)得到補碼
0010111 1011110 0011000

好像一徹都辣么美好, but, 如果是負數(shù)怎么辦?
-2
1111111111111111111111111111111111111111111111111111111111111110
答案接著往下看

六公壤、ZigZag編解碼

  • 編碼

對-2的補碼進行編碼
1111111111111111111111111111111111111111111111111111111111111110
1换可、先將符號位放最右邊
1111111111111111111111111111111111111111111111111111111111111101
2、取反(除符號位)
00000000000000000000000000000000000000000000000000000011
3厦幅、Varints編碼
0#0000011

  • 解碼

1沾鳄、Varints解碼(去掉高位的msg)
0000011
2、高位補0
00000000000000000000000000000000000000000000000000000011
5确憨、低位換高位
10000000000000000000000000000000000000000000000000000001
6译荞、取反(除符號位)
1111111111111111111111111111111111111111111111111111111111111110

七、Length-delimited

  • string , bytes :

    image.png

    Length-delimited類型編碼主要應(yīng)用類型string休弃、bytes吞歼、embedded messages、repeated玫芦,Length-delimited編碼結(jié)構(gòu)是TLV浆熔,在這里我們終于見到了Length了,也是唯一一個TLV結(jié)構(gòu)的編碼方式桥帆。
    String Value部分解碼參考:
    https://baike.baidu.com/item/ASCII/309296?fr=aladdin

  • embedded message


    image.png

嵌套message先解出第一層

  • Tag
    field = 00110 (4)
    wire_type = 010(2)
  • Length
    10 (2) 個字節(jié)
  • Value
    00001000 00000001
    再將Value按照上面的方式解析医增,只不過嵌套的message 里面只有一個wire_type = 0 編碼的一個數(shù)值,所以Length被省略了老虫。
  • repeated
    repeated 字符串和數(shù)值類型有一定區(qū)別叶骨,string和bytes 編碼結(jié)構(gòu)變?yōu)?Tag-Length-Value-Tag-Length-Value, 數(shù)值類型會進行打包處理,編碼結(jié)構(gòu)變?yōu)?Tag-Length-Value-Value-Value祈匙。
image.png

field_numbe = 00111(5)
wire_type= 010(2)
length = 011 (3字節(jié))
11010111 00001000 00000010
通過msb判斷可解析為 1#1010111 0#0001000 和 0#0000010
解析得到:
00010001010111(1111) 忽刽、0000010(2)

image.png

Tag-Length-Value-Tag-Length-Value則比較簡單天揖,他們的TAG都是一樣的,解析Tag跪帝,通過Length獲取對應(yīng)長度的字符Value即可

八今膊、32-bit、64-bit

這個比較簡單伞剑,32-bit斑唬、64-bit Value都是定長的分別是32bit(4個字節(jié)) 、64bit(8個字節(jié))黎泣,直接取得對應(yīng)的字節(jié)數(shù)即可恕刘,當(dāng)值通常大于 2的28次方和2的56次方,則比 uint32和uint64 更高效抒倚,因為Varints編碼每個字節(jié)會占用一個bit的msb(most significant bit)褐着。

九、如何落地托呕?

image.png

在SpringMVC進入業(yè)務(wù)方法前含蓉,會根據(jù)@RequestBody注解選擇適當(dāng)?shù)腍ttpMessageConverter實現(xiàn)類來將請求參數(shù)解析到string變量中,具體來說是使用了StringHttpMessageConverter類镣陕,它的canRead()方法返回true谴餐,然后它的read()方法會從請求中讀出請求參數(shù),綁定到業(yè)務(wù)方法的變量中呆抑。
當(dāng)SpringMVC執(zhí)行業(yè)務(wù)方法后岂嗓,由于返回值標識了@ResponseBody,SpringMVC將使用StringHttpMessageConverter的write()方法鹊碍,將業(yè)務(wù)方法的返回值寫入響應(yīng)報文厌殉,當(dāng)然,此時canWrite()方法返回true侈咕。

public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
    public static final Charset DEFAULT_CHARSET;
    public static final MediaType PROTOBUF;
    private static final Map<Class<?>, Method> methodCache;
    static {
        DEFAULT_CHARSET = StandardCharsets.UTF_8;
        PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET);
        methodCache = new ConcurrentReferenceHashMap();
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Message.class.isAssignableFrom(clazz);
    }


    @Override
    protected Message readInternal(Class<? extends Message> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        MediaType contentType = httpInputMessage.getHeaders().getContentType();

        Message.Builder builder = this.getMessageBuilder(aClass);
        if (PROTOBUF.isCompatibleWith(contentType)) {
            builder.mergeFrom(httpInputMessage.getBody());
        } else {
            Log.error("Request must be set Content-Type = application/x-protobuf");
            return null;
        }
        return builder.build();
    }

    @Override
    protected void writeInternal(Message message, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        MediaType contentType = httpOutputMessage.getHeaders().getContentType();
        if (PROTOBUF.isCompatibleWith(contentType)) {
            this.setProtoHeader(httpOutputMessage, message);
            //CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(httpOutputMessage.getBody());
            //message.writeTo(codedOutputStream);
            //codedOutputStream.flush();
            httpOutputMessage.getBody().write(message.toByteArray());
            httpOutputMessage.getBody().flush();
        }
    }
    private void setProtoHeader(HttpOutputMessage response, Message message) {
        response.getHeaders().set("X-Protobuf-Schema", message.getDescriptorForType().getFile().getName());
        response.getHeaders().set("X-Protobuf-Message", message.getDescriptorForType().getFullName());
    }

    private Message.Builder getMessageBuilder(Class<? extends Message> clazz) {
        try {
            Method method = (Method)methodCache.get(clazz);
            if (method == null) {
                method = clazz.getMethod("newBuilder");
                methodCache.put(clazz, method);
            }

            return (Message.Builder)method.invoke(clazz);
        } catch (Exception var3) {
            throw new HttpMessageConversionException("Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, var3);
        }
    }
}

具體落地細節(jié)請參考Url: https://github.com/anthony3669000/web_protobuf

十公罕、附錄

Protobuf數(shù)據(jù)類型

proto Notes C++ Java C#
double double double double
float float float float
int32 使用可變長度編碼。編碼負數(shù)的效率低 - 如果你的字段可能有負值耀销,請改用 sint32 int32 int int
int64 使用可變長度編碼楼眷。編碼負數(shù)的效率低 - 如果你的字段可能有負值,請改用 sint64 int64 long long
uint32 使用可變長度編碼(無符號) uint32 int uint
uint64 使用可變長度編碼(無符號 uint64 long ulong
sint32 使用可變長度編碼熊尉。有符號的 int 值罐柳。這些比常規(guī) int32 對負數(shù)能更有效地編碼 int32 int int
sint64 使用可變長度編碼。有符號的 long 值狰住。這些比常規(guī) int64 對負數(shù)能更有效地編碼 uint64 long long
fixed32 總是四個字節(jié)张吉。如果值通常大于 2的28次方,則比 uint32 更有效催植。 uint32 int uint
fixed64 總是八個字節(jié)肮蛹。如果值通常大于 2的56次方勺择,則比 uint64 更有效。 uint64 long ulong
sfixed32 總是四個字節(jié) int32 int int
sfixed64 總是八個字節(jié) int64 long long
bool bool boolean boolean
string 字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本 string String string
bytes 可以包含任意字節(jié)序列 string ByteString string

十一伦忠、參考資料

https://github.com/anthony3669000/web_protobuf

https://github.com/protocolbuffers/protobuf/releases

https://developers.google.com/protocol-buffers/docs/overview

https://my.oschina.net/lichhao/blog/172562

http://www.reibang.com/p/73c9ed3a4877

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末省核,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子昆码,更是在濱河造成了極大的恐慌芳撒,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件未桥,死亡現(xiàn)場離奇詭異,居然都是意外死亡芥备,警方通過查閱死者的電腦和手機冬耿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萌壳,“玉大人亦镶,你說我怎么就攤上這事「の停” “怎么了缤骨?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尺借。 經(jīng)常有香客問我绊起,道長,這世上最難降的妖魔是什么燎斩? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任虱歪,我火速辦了婚禮,結(jié)果婚禮上栅表,老公的妹妹穿的比我還像新娘笋鄙。我一直安慰自己,他們只是感情好怪瓶,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布萧落。 她就那樣靜靜地躺著,像睡著了一般洗贰。 火紅的嫁衣襯著肌膚如雪找岖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天哆姻,我揣著相機與錄音宣增,去河邊找鬼。 笑死矛缨,一個胖子當(dāng)著我的面吹牛爹脾,可吹牛的內(nèi)容都是我干的帖旨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼灵妨,長吁一口氣:“原來是場噩夢啊……” “哼解阅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起泌霍,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤货抄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后朱转,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蟹地,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年藤为,在試婚紗的時候發(fā)現(xiàn)自己被綠了怪与。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡缅疟,死狀恐怖分别,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情存淫,我是刑警寧澤耘斩,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站桅咆,受9級特大地震影響括授,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轧邪,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一刽脖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧忌愚,春花似錦曲管、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至简十,卻和暖如春檬某,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背螟蝙。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工恢恼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胰默。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓场斑,卻偏偏與公主長得像漓踢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子漏隐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359