小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Protocol Buffer 序列化原理大揭秘

 Baruch 2017-04-26

前言

  • 習(xí)慣用 Json、XML 數(shù)據(jù)存儲(chǔ)格式的你們,相信大多都沒(méi)聽(tīng)過(guò)Protocol Buffer
  • Protocol Buffer 其實(shí) 是 Google出品的一種輕量 & 高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式,性能比 Json、XML 真的強(qiáng)!太!多!

    由于 Google出品,我相信Protocol Buffer已經(jīng)具備足夠的吸引力

  • 今天,我將講解為什么Protocol Buffer的性能如此的好:
    a. 序列化速度 & 反序列化速度快
    b. 數(shù)據(jù)壓縮效果好,即序列化后的數(shù)據(jù)量體積小

閱讀本文前請(qǐng)先閱讀:
1. 快來(lái)看看Google出品的Protocol Buffer,別只會(huì)用Json和XML了
2. 手把手教你如何安裝Protocol Buffer
3. 這是一份很有誠(chéng)意的 Protocol Buffer 語(yǔ)法詳解


目錄

目錄


1. 定義

一種 結(jié)構(gòu)化數(shù)據(jù) 的數(shù)據(jù)存儲(chǔ)格式(類似于 XML、Json

  1. Google 出品 (開(kāi)源)
  2. Protocol Buffer 目前有兩個(gè)版本:proto2proto3
  3. 因?yàn)?code>proto3 還是beta 版,所以本次講解是 proto2

2. 作用

通過(guò)將 結(jié)構(gòu)化的數(shù)據(jù) 進(jìn)行 串行化(序列化),從而實(shí)現(xiàn) 數(shù)據(jù)存儲(chǔ) / RPC 數(shù)據(jù)交換的功能

  1. 序列化: 將 數(shù)據(jù)結(jié)構(gòu)或?qū)ο?轉(zhuǎn)換成 二進(jìn)制串 的過(guò)程
  2. 反序列化:將在序列化過(guò)程中所生成的二進(jìn)制串 轉(zhuǎn)換成 數(shù)據(jù)結(jié)構(gòu)或者對(duì)象 的過(guò)程

3. 特點(diǎn)

  • 對(duì)比于 常見(jiàn)的 XML、Json 數(shù)據(jù)存儲(chǔ)格式,Protocol Buffer有如下特點(diǎn):

Protocol Buffer 特點(diǎn)


4. 應(yīng)用場(chǎng)景

傳輸數(shù)據(jù)量大 & 網(wǎng)絡(luò)環(huán)境不穩(wěn)定 的數(shù)據(jù)存儲(chǔ)、RPC 數(shù)據(jù)交換 的需求場(chǎng)景

如 即時(shí)IM (QQ、微信)的需求場(chǎng)景


總結(jié)

傳輸數(shù)據(jù)量較大的需求場(chǎng)景下,Protocol BufferXML、Json 更小、更快、使用 & 維護(hù)更簡(jiǎn)單!


5. 使用流程

關(guān)于 Protocol Buffer 的使用流程,具體請(qǐng)看我寫(xiě)的文章:快來(lái)看看Google出品的Protocol Buffer,別只會(huì)用Json和XML了


6. 知識(shí)基礎(chǔ)

6.1 網(wǎng)絡(luò)通信協(xié)議

  • 序列化 & 反序列化 屬于通訊協(xié)議的一部分
  • 通訊協(xié)議采用分層模型:TCP/IP模型(四層) & OSI 模型 (七層)

通信協(xié)議結(jié)構(gòu)

  • 序列化 / 反序列化 屬于 TCP/IP模型 應(yīng)用層 和 OSI`模型 展示層的主要功能:

    1. (序列化)把 應(yīng)用層的對(duì)象 轉(zhuǎn)換成 二進(jìn)制串
    2. (反序列化)把 二進(jìn)制串 轉(zhuǎn)換成 應(yīng)用層的對(duì)象
  • 所以, Protocol Buffer屬于 TCP/IP模型的應(yīng)用層 & OSI模型的展示層

6.2 數(shù)據(jù)結(jié)構(gòu)、對(duì)象與二進(jìn)制串

不同的計(jì)算機(jī)語(yǔ)言中,數(shù)據(jù)結(jié)構(gòu),對(duì)象以及二進(jìn)制串的表示方式并不相同。

a. 對(duì)于數(shù)據(jù)結(jié)構(gòu)和對(duì)象

  • 對(duì)于面向?qū)ο蟮恼Z(yǔ)言(如Java):對(duì)象 = Object = 類的實(shí)例化;在Java中最接近數(shù)據(jù)結(jié)構(gòu) 即 POJOPlain Old Java Object),或Javabean(只有 setter/getter 方法的類)

  • 對(duì)于半面向?qū)ο蟮恼Z(yǔ)言(如C++),對(duì)象 = class,數(shù)據(jù)結(jié)構(gòu) = struct

b. 二進(jìn)制串

  • 對(duì)于C++,因?yàn)榫哂袃?nèi)存操作符,所以 二進(jìn)制串 容易理解:C++的字符串可以直接被傳輸層使用,因?yàn)槠浔举|(zhì)上就是以 '\0' 結(jié)尾的存儲(chǔ)在內(nèi)存中的二進(jìn)制串
  • 對(duì)于 Java,二進(jìn)制串 = 字節(jié)數(shù)組 =byte[]
    1. byte 屬于 Java 的八種基本數(shù)據(jù)類型
    2. 二進(jìn)制串 容易和 String混淆:String 一種特殊對(duì)象(Object)。對(duì)于跨語(yǔ)言間的通訊,序列化后的數(shù)據(jù)當(dāng)然不能是某種語(yǔ)言的特殊數(shù)據(jù)類型。

6.3 T - L - V 的數(shù)據(jù)存儲(chǔ)方式

  • 定義
    Tag - Length - Value,標(biāo)識(shí) - 長(zhǎng)度 - 字段值 存儲(chǔ)方式

  • 作用
    標(biāo)識(shí) - 長(zhǎng)度 - 字段值 表示單個(gè)數(shù)據(jù),最終將所有數(shù)據(jù)拼接成一個(gè) 字節(jié)流,從而 實(shí)現(xiàn) 數(shù)據(jù)存儲(chǔ) 的功能

    其中 Length可選存儲(chǔ),如 儲(chǔ)存Varint編碼數(shù)據(jù)就不需要存儲(chǔ)Length

  • 示意圖

最終存儲(chǔ)的字節(jié)流

  • 優(yōu)點(diǎn)
    從上圖可知,T - L - V 存儲(chǔ)方式的優(yōu)點(diǎn)是
    1. 不需要分隔符 就能 分隔開(kāi)字段,減少了 分隔符 的使用
    2. 各字段 存儲(chǔ)得非常緊湊,存儲(chǔ)空間利用率非常高
    3. 若 字段沒(méi)有被設(shè)置字段值,那么該字段在序列化時(shí)的數(shù)據(jù)中是完全不存在的,即不需要編碼

      相應(yīng)字段在解碼時(shí)才會(huì)被設(shè)置為默認(rèn)值


7. 序列化原理解析

請(qǐng)記住Protocol Buffer三個(gè)關(guān)于數(shù)據(jù)存儲(chǔ) 的重要結(jié)論:

  • 結(jié)論1: Protocol Buffer將 消息里的每個(gè)字段 進(jìn)行編碼后,再利用T - L - V 存儲(chǔ)方式 進(jìn)行數(shù)據(jù)的存儲(chǔ),最終得到的是一個(gè) 二進(jìn)制字節(jié)流

    序列化 = 對(duì)數(shù)據(jù)進(jìn)行編碼 + 存儲(chǔ)

  • 結(jié)論2:Protocol Buffer對(duì)于不同數(shù)據(jù)類型 采用不同的 序列化方式(編碼方式 & 數(shù)據(jù)存儲(chǔ)方式),如下圖:
    數(shù)據(jù)類型 對(duì)應(yīng)的編碼方式

從上表可以看出:
1. 對(duì)于存儲(chǔ)Varint編碼數(shù)據(jù),就不需要存儲(chǔ)字節(jié)長(zhǎng)度 Length,所以實(shí)際上Protocol Buffer的存儲(chǔ)方式是 T - V;
2. 若Protocol Buffer采用其他編碼方式(如LENGTH_DELIMITED)則采用T - L - V

  • 結(jié)論3:因?yàn)?Protocol Buffer對(duì)于數(shù)據(jù)字段值的 獨(dú)特編碼方式 & T - L - V數(shù)據(jù)存儲(chǔ)方式,使得 Protocol Buffer序列化后數(shù)據(jù)量體積如此小

下面,我將對(duì)不同的編碼方式 & 數(shù)據(jù)存儲(chǔ)方式進(jìn)行逐一講解。


7.1 Wire Type = 0時(shí)的編碼 & 數(shù)據(jù)存儲(chǔ)方式

Wire Type = 0時(shí)的編碼 & 數(shù)據(jù)存儲(chǔ)方式

7.1.1 編碼方式: Varint & Zigzag

A. Varint編碼方式介紹

i. 簡(jiǎn)介

  • 定義:一種變長(zhǎng)的編碼方式
  • 原理:用字節(jié) 表示 數(shù)字:值越小的數(shù)字,使用越少的字節(jié)數(shù)表示
  • 作用:通過(guò)減少 表示數(shù)字 的字節(jié)數(shù) 從而進(jìn)行數(shù)據(jù)壓縮
    如:
    - 對(duì)于 int32 類型的數(shù)字,一般需要 4個(gè)字節(jié) 表示;
    2. 若采用 Varint編碼,對(duì)于很小的 int32 類型 數(shù)字,則可以用 1個(gè)字節(jié) 來(lái)表示
    3. 雖然大的數(shù)字會(huì)需要 5 個(gè) 字節(jié) 來(lái)表示,但大多數(shù)情況下,消息都不會(huì)有很大的數(shù)字,所以采用 Varint方法總是可以用更少的字節(jié)數(shù)來(lái)表示數(shù)字

ii. 原理介紹

  • 源碼分析
private void writeVarint32(int n) {                                                                                    
  int idx = 0;  
  while (true) {  
    if ((n & ~0x7F) == 0) {  
      i32buf[idx++] = (byte)n;  
      break;  
    } else {  
      i32buf[idx++] = (byte)((n & 0x7F) | 0x80);  
      // 步驟1:取出字節(jié)串末7位
      // 對(duì)于上述取出的7位:在最高位添加1構(gòu)成一個(gè)字節(jié)
      // 如果是最后一次取出,則在最高位添加0構(gòu)成1個(gè)字節(jié)

      n >>>= 7;  
      // 步驟2:通過(guò)將字節(jié)串整體往右移7位,繼續(xù)從字節(jié)串的末尾選取7位,直到取完為止。
    }  
  }  
  trans_.write(i32buf, 0, idx); 
      // 步驟3: 將上述形成的每個(gè)字節(jié) 按序拼接 成一個(gè)字節(jié)串
      // 即該字節(jié)串就是經(jīng)過(guò)Varint編碼后的字節(jié)
}   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

從上面可看出:Varint 中每個(gè) 字節(jié) 的最高位 都有特殊含義:

  • 如果是 1,表示后續(xù)的 字節(jié) 也是該數(shù)字的一部分
  • 如果是 0,表示這是最后一個(gè)字節(jié),且剩余 7位 都用來(lái)表示數(shù)字

    所以,當(dāng)使用Varint解碼時(shí)時(shí),只要讀取到最高位為0的字節(jié)時(shí),就表示已經(jīng)是Varint的最后一個(gè)字節(jié)


因此:
  • 小于 128 的數(shù)字 都可以用 1個(gè)字節(jié) 表示;
  • 大于 128 的數(shù)字,比如 300,會(huì)用兩個(gè)字節(jié)來(lái)表示:10101100 00000010

下面,我將用兩個(gè)個(gè)例子來(lái)說(shuō)明Varint編碼方式的使用

  • 目的:對(duì) 數(shù)據(jù)類型為Int32 的 字段值為296 和字段值為104 進(jìn)行Varint編碼
  • 以下是編碼過(guò)程

Varint編碼過(guò)程

從上面可以看出:

  • 對(duì)于 int32 類型的數(shù)字,一般需要 4 個(gè)字節(jié) 來(lái)表示;
  • 但采用 Varint 方法,對(duì)于很小的 Int32 類型 數(shù)字(小于256),則可以用 1個(gè)字節(jié) 來(lái)表示;

    以此類推,比如300也只需要2個(gè)字節(jié)

  • 雖然大的數(shù)字會(huì)需要 5 個(gè)字節(jié) 來(lái)表示,但大多數(shù)情況下,消息都不會(huì)有很大的數(shù)字

  • 所以采用 Varint 方法總是可以用更少的字節(jié)數(shù)來(lái)表示數(shù)字,從而更好地實(shí)現(xiàn)數(shù)據(jù)壓縮

下面繼續(xù)看如何解析經(jīng)過(guò)Varint 編碼的字節(jié)

Varint 編碼方式的不足

  • 背景:在計(jì)算機(jī)內(nèi),負(fù)數(shù)一般會(huì)被表示為很大的整數(shù)

    因?yàn)橛?jì)算機(jī)定義負(fù)數(shù)的符號(hào)位為數(shù)字的最高位

  • 問(wèn)題:如果采用 Varint編碼方式 表示一個(gè)負(fù)數(shù),那么一定需要 5 個(gè) byte(因?yàn)樨?fù)數(shù)的最高位是1,會(huì)被當(dāng)做很大的整數(shù)去處理)
  • 解決方案: Protocol Buffer 定義了 sint32 / sint64 類型表示負(fù)數(shù),通過(guò)先采用 Zigzag 編碼(將 有符號(hào)數(shù) 轉(zhuǎn)換成 無(wú)符號(hào)數(shù)),再采用 Varint編碼,從而用于減少編碼后的字節(jié)數(shù)
    表示負(fù)數(shù)時(shí)采用Zigzag編碼

1. 對(duì)于int32 / int64 類型的字段值(正數(shù)),Protocol Buffer直接采用 Varint編碼
2. 對(duì)于sint32 / sint64 類型的字段值(負(fù)數(shù)),Protocol Buffer會(huì)先采用 Zigzag 編碼,再采用 Varint編碼
  • 總結(jié):為了更好地減少 表示負(fù)數(shù)時(shí) 的字節(jié)數(shù),Protocol BufferVarint編碼上又增加了Zigzag 編碼方式進(jìn)行編碼
  • 下面將詳細(xì)介紹 Zigzag編碼方式

B. Zigzag編碼方式詳解

i. 簡(jiǎn)介

  • 定義:一種變長(zhǎng)的編碼方式
  • 原理:使用 無(wú)符號(hào)數(shù) 來(lái)表示 有符號(hào)數(shù)字;
  • 作用:使得絕對(duì)值小的數(shù)字都可以采用較少 字節(jié) 來(lái)表示;
    特別是對(duì) 表示負(fù)數(shù)的數(shù)據(jù) 能更好地進(jìn)行數(shù)據(jù)壓縮

b. 原理

  • 源碼分析

public int int_to_zigzag(int n)
// 傳入的參數(shù)n = 傳入字段值的二進(jìn)制表示(此處以負(fù)數(shù)為例)
// 負(fù)數(shù)的二進(jìn)制 = 符號(hào)位為1,剩余的位數(shù)為 該數(shù)絕對(duì)值的原碼按位取反;然后整個(gè)二進(jìn)制數(shù)+1
{
        return (n <<1) ^ (n >>31);   
        // 對(duì)于sint 32 數(shù)據(jù)類型,使用Zigzag編碼過(guò)程如下:
        // 1. 將二進(jìn)制表示數(shù) 左移1位(左移 = 整個(gè)二進(jìn)制左移,低位補(bǔ)0)
        // 2. 將二進(jìn)制表示數(shù) 右移31位 
              // 對(duì)于右移:
              // 首位是1的二進(jìn)制(有符號(hào)數(shù)),是算數(shù)右移,即右移后左邊補(bǔ)1
              // 首位是0的二進(jìn)制(無(wú)符號(hào)數(shù)),是邏輯左移,即右移后左邊補(bǔ)0
        // 3. 將上述二者進(jìn)行異或

        // 對(duì)于sint 64 數(shù)據(jù)類型 則為: return  (n << 1> ^ (n >> 63) ;
}


// 附:將Zigzag值解碼為整型值
public int zigzag_to_int(int n) 
{
        return(n >>> 1) ^ -(n & 1);
// 右移時(shí),需要用不帶符號(hào)的移動(dòng),否則如果第一位數(shù)據(jù)位是1的話,就會(huì)補(bǔ)1
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 實(shí)例說(shuō)明:將 -2進(jìn)行 Zigzag編碼:

Zigzag編碼

  • Zigzag 編碼 是補(bǔ)充 Varint編碼在 表示負(fù)數(shù) 的不足,從而更好的幫助 Protocol Buffer進(jìn)行數(shù)據(jù)的壓縮
  • 所以,如果提前預(yù)知字段值是可能取負(fù)數(shù)的時(shí)候,記得采用sint32 / sint64 數(shù)據(jù)類型

總結(jié)

Protocol Buffer 通過(guò)VarintZigzag編碼后大大減少了字段值占用字節(jié)數(shù)。

7.1.2 存儲(chǔ)方式:T - V

  • 消息字段的標(biāo)識(shí)號(hào)、數(shù)據(jù)類型 & 字段值經(jīng)過(guò) Protocol Buffer采用 Varint & Zigzag 編碼后,以 T - V 方式進(jìn)行數(shù)據(jù)存儲(chǔ)

    對(duì)于 Varint & Zigzag 編碼,省略了T - L - V中的字節(jié)長(zhǎng)度Length


Varint & Zigzag數(shù)據(jù)存儲(chǔ)方式 下面將詳細(xì)介紹`T - V` 存儲(chǔ)方式中的存儲(chǔ)細(xì)節(jié):`Tag` & `Value`

1. Tag

  • 定義:經(jīng)過(guò) Protocol Buffer采用Varint & Zigzag編碼后 的消息字段 標(biāo)識(shí)號(hào) & 數(shù)據(jù)類型 的值
  • 作用:標(biāo)識(shí) 字段

    1. 存儲(chǔ)了字段的標(biāo)識(shí)號(hào)(field_number)和 數(shù)據(jù)類型(wire_type),即Tag = 字段數(shù)據(jù)類型(wire_type) + 標(biāo)識(shí)號(hào)(field_number
    2. 占用 一個(gè)字節(jié) 的長(zhǎng)度(如果標(biāo)識(shí)號(hào)超過(guò)了16,則占用多一個(gè)字節(jié)的位置)
    3. 解包時(shí),Protocol Buffer根據(jù) TagValue 對(duì)應(yīng)于消息中的 字段
  • 具體使用

// Tag 的具體表達(dá)式如下
 Tag  = (field_number << 3) | wire_type
// 參數(shù)說(shuō)明:
// field_number:對(duì)應(yīng)于 .proto文件中消息字段的標(biāo)識(shí)號(hào),表示這是消息里的第幾個(gè)字段
// field_number << 3:表示 field_number = 將 Tag的二進(jìn)制表示 右移三位 后的值 
// field_num左移3位不會(huì)導(dǎo)致數(shù)據(jù)丟失,因?yàn)楸硎痉秶€是足夠大地去表示消息里的字段數(shù)目

//  wire_type:表示 字段 的數(shù)據(jù)類型
//  wire_type = Tag的二進(jìn)制表示 的最低三位值
//   wire_type的取值
 enum WireType { 
      WIRETYPE_VARINT = 0, 
      WIRETYPE_FIXED64 = 1, 
      WIRETYPE_LENGTH_DELIMITED = 2, 
      WIRETYPE_START_GROUP = 3, 
      WIRETYPE_END_GROUP = 4, 
      WIRETYPE_FIXED32 = 5
   };

// 從上面可以看出,`wire_type`最多占用 3位 的內(nèi)存空間(因?yàn)?3位 足以表示 0-5 的二進(jìn)制)

//  以下是 wire_type 對(duì)應(yīng)的 數(shù)據(jù)類型 表
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

wire_type對(duì)應(yīng)數(shù)據(jù)類型

  • 實(shí)例說(shuō)明
// 消息對(duì)象
 message person
 { 
    required int32     id = 1;  
    // wire type = 0,field_number =1 
    required string    name = 2;  
    // wire type = 2,field_number =2 
  }

//  如果一個(gè)Tag的二進(jìn)制 = 0001 0010
標(biāo)識(shí)號(hào) = field_number = field_number  << 3 =右移3位 =  0000 0010 = 2
數(shù)據(jù)類型 = wire_type = 最低三位表示 = 010 = 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2. Value

經(jīng)過(guò) Protocol Buffer采用Varint & Zigzag編碼后 的消息字段的值

7.1.3 實(shí)例說(shuō)明

下面通過(guò)一個(gè)實(shí)例進(jìn)行整個(gè)編碼過(guò)程的說(shuō)明:

  • 消息說(shuō)明
message Test
{

required int32 id1 = 1;

required int32 id2 = 2;
}

// 在代碼中給id1 附上1個(gè)字段值:296
// 在代碼中給id2 附上1個(gè)字段值:296
Test.setId1(300);
Test.setId2(296);

// 編碼結(jié)果為:二進(jìn)制字節(jié)流 = [8,-84,2,16, -88, 2]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 整個(gè)編碼過(guò)程如下

編碼過(guò)程


7.2 Wire Type = 1& 5時(shí)的編碼&數(shù)據(jù)存儲(chǔ)方式

Wire Type = 1& 5時(shí)的編碼&數(shù)據(jù)存儲(chǔ)方式

  • 64(32)-bit編碼方式較簡(jiǎn)單:編碼后的數(shù)據(jù)具備固定大小 = 64位(8字節(jié)) / 32位(4字節(jié))
    兩種情況下,都是高位在后,低位在前
  • 采用T - V方式進(jìn)行數(shù)據(jù)存儲(chǔ),同上。

7.3 Wire Type = 2時(shí)的 編碼 & 數(shù)據(jù)存儲(chǔ)方式

Wire Type = 2時(shí)的編碼&數(shù)據(jù)存儲(chǔ)方式

  • 對(duì)于編碼方式:

編碼方式

  • 數(shù)據(jù)存儲(chǔ)方式: T - L - V

數(shù)據(jù)存儲(chǔ)示意圖

此處主要講解三種數(shù)據(jù)類型:

  • String類型
  • 嵌套消息類型(Message
  • 通過(guò)packed修飾的 repeat 字段(即packed repeated fields

1. String類型

字段值(即V) 采用UTF-8編碼

編碼 & 存儲(chǔ)方式

  • 例子:
message Test2
{
    required string str = 2;
}

// 將str設(shè)置為:testing
Test2.setStr(“testing”)

// 經(jīng)過(guò)protobuf編碼序列化后的數(shù)據(jù)以二進(jìn)制的方式輸出
// 輸出為:18, 7, 116, 101, 115, 116, 105, 110, 103
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

實(shí)例

2. 嵌套消息類型(Message)

  • 存儲(chǔ)方式:T - L - V
    1. 外部消息的 V即為 嵌套消息的字段
    2. T - L -V 里嵌套了一系列的 T - L -V
    3. 編碼方式:字段值(即V) 根據(jù)字段的數(shù)據(jù)類型采用不同編碼方式

編碼 & 存儲(chǔ)方式
  • 實(shí)例
    定義如下嵌套消息:
message Test2
{
    required string str = 1;
    required int32 id1 = 2;


}

message Test3 {
  required Test2 c = 1;
}

// 將Test2中的字段str設(shè)置為:testing
// 將Test2中的字段id1設(shè)置為:296
// 編碼后的字節(jié)為:10 ,12 ,18,7,116, 101, 115, 116, 105, 110, 103,16,-88,2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

編碼 & 存儲(chǔ)方式如下

編碼 & 存儲(chǔ)方式

3. 通過(guò)packed修飾的 repeat 字段

repeated 修飾的字段有兩種表達(dá)方式:

message Test
{
    repeated int32 Car = 4 ;
    // 表達(dá)方式1:不帶packed=true

    repeated int32 Car = 4 [packed=true];
    // 表達(dá)方式2:帶packed=true
    // proto 2.1 開(kāi)始可使用

// 區(qū)別在于:是否連續(xù)存儲(chǔ)repeated類型數(shù)據(jù)
}


// 在代碼中給`repeated int32 Car`附上3個(gè)字段值:3、270、86942

Test.setCar(3);
Test.setCar(270);
Test.setCar(86942);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 背景:對(duì)于同一個(gè) repeated字段、多個(gè)字段值來(lái)說(shuō),他們的Tag都是相同的,即數(shù)據(jù)類型 & 標(biāo)識(shí)號(hào)都相同

    repeated類型可以看成是數(shù)組

  • 問(wèn)題:若以傳統(tǒng)的多個(gè) T - V對(duì)存儲(chǔ)(不帶packed=true),則會(huì)導(dǎo)致Tag的冗余,即相同的Tag存儲(chǔ)多次;

不帶pack的存儲(chǔ)方式

  • 解決方案:采用帶packed=truerepeated 字段存儲(chǔ)方式,即將相同的 Tag 只存儲(chǔ)一次、添加 repeated 字段下所有字段值的長(zhǎng)度Length、連續(xù)存儲(chǔ) repeated 字段值,組成一個(gè)大的Tag - Length - Value -Value -Value對(duì),即T - L - V - V - V對(duì)。

帶pack的存儲(chǔ)方式

通過(guò)采用帶packed=truerepeated 字段存儲(chǔ)方式,從而更好地壓縮序列化后的數(shù)據(jù)長(zhǎng)度。

特別注意

  • Protocol Bufferpacked修飾只用于repeated字段 或 基本類型的repeated字段
  • 用在其他字段,編譯 .proto 文件時(shí)會(huì)報(bào)錯(cuò)

8. 特別注意

  • 注意1:若 required字段沒(méi)有被設(shè)置字段值,那么在IsInitialized()進(jìn)行初始化檢查會(huì)報(bào)錯(cuò)并提示失敗

    所以 required字段必須要被設(shè)置字段值

  • 注意2:序列化順序 是根據(jù) Tag標(biāo)識(shí)號(hào) 從小到大 進(jìn)行編碼

    .proto文件內(nèi) 字段定義的數(shù)據(jù)無(wú)關(guān)

  • 注意3:T - V的數(shù)據(jù)存儲(chǔ)方式保證了Protobuf的版本兼容:高<->低 或 低<->高都可以適配

    若新版本 增加了 required 字段, 舊版本 在數(shù)據(jù)解碼時(shí)會(huì)認(rèn)為IsInitialized() 失敗,所以慎用 required字段


9. 使用建議

根據(jù)上面的序列化原理分析,我總結(jié)出以下Protocol Buffer的使用建議

通過(guò)下面建議能有效降低序列化后數(shù)據(jù)量的大小

  • 建議1:多用 optionalrepeated修飾符
    因?yàn)槿?code>optional 或 repeated 字段沒(méi)有被設(shè)置字段值,那么該字段在序列化時(shí)的數(shù)據(jù)中是完全不存在的,即不需要進(jìn)行編碼

    相應(yīng)的字段在解碼時(shí)才會(huì)被設(shè)置為默認(rèn)值

  • 建議2:字段標(biāo)識(shí)號(hào)(Field_Number)盡量只使用 1-15,且不要跳動(dòng)使用
    因?yàn)?code>Tag里的Field_Number是需要占字節(jié)空間的。如果Field_Number>16時(shí),Field_Number的編碼就會(huì)占用2個(gè)字節(jié),那么Tag在編碼時(shí)也就會(huì)占用更多的字節(jié);如果將字段標(biāo)識(shí)號(hào)定義為連續(xù)遞增的數(shù)值,將獲得更好的編碼和解碼性能

  • 建議3:若需要使用的字段值出現(xiàn)負(fù)數(shù),請(qǐng)使用 sint32 / sint64,不要使用int32 / int64
    因?yàn)椴捎?code>sint32 / sint64數(shù)據(jù)類型表示負(fù)數(shù)時(shí),會(huì)先采用Zigzag編碼再采用Varint編碼,從而更加有效壓縮數(shù)據(jù)

  • 建議4:對(duì)于repeated字段,盡量增加packed=true修飾
    因?yàn)榧恿?code>packed=true修飾repeated字段采用連續(xù)數(shù)據(jù)存儲(chǔ)方式,即T - L - V - V -V方式


10. 序列化 & 反序列化過(guò)程

  • Protocol Buffer除了序列化 & 反序列化后的數(shù)據(jù)體積小,序列化 & 反序列化的速度也非常快
  • 下面我將講解序列化 & 反序列化的序列化過(guò)程

10.1 Protocol Buffer的序列化 & 反序列化過(guò)程

序列化過(guò)程如下:
1. 判斷每個(gè)字段是否有設(shè)置值,有值才進(jìn)行編碼
2. 根據(jù) 字段標(biāo)識(shí)號(hào)&數(shù)據(jù)類型 將 字段值 通過(guò)不同的編碼方式進(jìn)行編碼

由于:
a. 編碼方式簡(jiǎn)單(只需要簡(jiǎn)單的數(shù)學(xué)運(yùn)算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成

所以Protocol Buffer的序列化速度非???。

反序列化過(guò)程如下:
1. 調(diào)用 消息類的 parseFrom(input) 解析從輸入流讀入的二進(jìn)制字節(jié)數(shù)據(jù)流

從上面可知,Protocol Buffer解析過(guò)程只需要通過(guò)簡(jiǎn)單的解碼方式即可完成,無(wú)需復(fù)雜的詞法語(yǔ)法分析,因此 解析速度非???。
2. 將解析出來(lái)的數(shù)據(jù) 按照指定的格式讀取到 Java、C++、Phyton 對(duì)應(yīng)的結(jié)構(gòu)類型中

由于:
a. 解碼方式簡(jiǎn)單(只需要簡(jiǎn)單的數(shù)學(xué)運(yùn)算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成

所以Protocol Buffer的反序列化速度非???。

10.2 對(duì)比于XML 的序列化 & 反序列化過(guò)程

XML的反序列化過(guò)程如下:
1. 從文件中讀取出字符串
2. 將字符串轉(zhuǎn)換為 XML 文檔對(duì)象結(jié)構(gòu)模型
3. 從 XML 文檔對(duì)象結(jié)構(gòu)模型中讀取指定節(jié)點(diǎn)的字符串
4. 將該字符串轉(zhuǎn)換成指定類型的變量

上述過(guò)程非常復(fù)雜,其中,將 XML 文件轉(zhuǎn)換為文檔對(duì)象結(jié)構(gòu)模型的過(guò)程通常需要完成詞法文法分析等大量消耗 CPU 的復(fù)雜計(jì)算。

因?yàn)樾蛄谢?& 反序列化過(guò)程簡(jiǎn)單,所以序列化 & 反序列化過(guò)程速度非??欤@也是 Protocol Buffer效率高的原因


11.總結(jié)

  • Protocol Buffer的序列化 & 反序列化簡(jiǎn)單 & 速度快的原因是:
    a. 編碼 / 解碼 方式簡(jiǎn)單(只需要簡(jiǎn)單的數(shù)學(xué)運(yùn)算 = 位移等等)
    b. 采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成

  • Protocol Buffer的數(shù)據(jù)壓縮效果好(即序列化后的數(shù)據(jù)量體積?。┑脑蚴牵?
    a. 采用了獨(dú)特的編碼方式,如VarintZigzag編碼方式等等
    b. 采用T - L - V 的數(shù)據(jù)存儲(chǔ)方式:減少了分隔符的使用 & 數(shù)據(jù)存儲(chǔ)得緊湊


接下來(lái)我會(huì)講解Protocol Buffer的源碼分析,有興趣可以繼續(xù)關(guān)注Carson_Ho的安卓開(kāi)發(fā)筆記


請(qǐng)幫頂或評(píng)論點(diǎn)贊!因?yàn)槟愕墓膭?lì)是我寫(xiě)作的最大動(dòng)力!

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多