Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 499, comments - 13, trackbacks - 0, articles - 1

          本文由騰訊PCG后臺(tái)開發(fā)工程師的SG4YK分享,進(jìn)行了修訂和和少量改動(dòng)。

          1、引言

          近日學(xué)習(xí)了 Protobuf 的編碼實(shí)現(xiàn)技術(shù)原理,借此機(jī)會(huì),正好總結(jié)一下并整理成文。

          接上篇由淺入深,從根上理解Protobuf的編解碼原理》,本篇將從Base64再到Base128編碼,帶你一起從底層來理解Protobuf的數(shù)據(jù)編碼原理。

          本文結(jié)構(gòu)總體與 Protobuf 官方文檔相似,不少內(nèi)容也來自官方文檔,并在官方文檔的基礎(chǔ)上添加作者理解的內(nèi)容(確保不那么枯燥),如有出入請(qǐng)以官方文檔為準(zhǔn)。

          學(xué)習(xí)交流:

          (本文已同步發(fā)布于:http://www.52im.net/thread-4093-1-1.html

          2、系列文章

          本文是系列文章中的第 篇,本系列總目錄如下:

          3、寫在前面

          在上篇《由淺入深,從根上理解Protobuf的編解碼原理》中,我們已經(jīng)由淺入深地探討了Protobuf的編解碼技術(shù)實(shí)現(xiàn)實(shí)現(xiàn),但實(shí)際上在初學(xué)者看來,Protobuf的編碼原理到底源自哪里又去向何方,看起來還是有點(diǎn)蒙,我們或許可以從Protobuf與經(jīng)典字符編碼技術(shù)的關(guān)系上能更好的理解這些。

          好了,不賣關(guān)子了。。。

          實(shí)際上,Protobuf 的編碼是基于變種的 Base128的。

          在學(xué)習(xí) Protobuf 編碼或是 Base128 之前,我們先來了解下 Base64 編碼。

          4、什么是Base 64

          4.1 技術(shù)背景

          當(dāng)我們在計(jì)算機(jī)之間傳輸數(shù)據(jù)時(shí),數(shù)據(jù)本質(zhì)上是一串字節(jié)流。

          TCP 協(xié)議可以保證被發(fā)送的字節(jié)流正確地達(dá)到目的地(至少在出錯(cuò)時(shí)有一定的糾錯(cuò)機(jī)制),所以本文不討論因網(wǎng)絡(luò)因素造成的數(shù)據(jù)損壞。

          但數(shù)據(jù)到達(dá)目標(biāo)機(jī)器之后,由于不同機(jī)器采用的字符集不同等原因,我們并不能保證目標(biāo)機(jī)器能夠正確地“理解”字節(jié)流。

          Base 64 最初被設(shè)計(jì)用于在郵件中嵌入文件(作為 MIME 的一部分):它可以將任何形式的字節(jié)流編碼為“安全”的字節(jié)流。

          何為“安全“的字節(jié)?先來看看 Base 64 是如何工作的。

          4.2 工作原理

          假設(shè)這里有四個(gè)字節(jié),代表你要傳輸?shù)亩M(jìn)制數(shù)據(jù):

          首先將這字節(jié)流按每 6 個(gè) bit 為一組進(jìn)行分組,剩下少于 6 bits 的低位補(bǔ) 0:

          然后在每一組 6 bits 的高位補(bǔ)兩個(gè) 0:

          下面這張圖是 Base 64 的編碼對(duì)照表:

          對(duì)照 Base 64 的編碼對(duì)照表,字節(jié)流可以用ognC0w來表示。

          另外:Base64 編碼是按照 6 bits 為一組進(jìn)行編碼,每 3 個(gè)字節(jié)的原始數(shù)據(jù)要用 4 個(gè)字節(jié)來儲(chǔ)存,編碼后的長度要為 4 的整數(shù)倍,不足 4 字節(jié)的部分要使用 pad 補(bǔ)齊,所以最終的編碼結(jié)果為ognC0w==。

          任意的字節(jié)流均可以使用 Base 64 進(jìn)行編碼,編碼之后所有字節(jié)均可以用數(shù)字、字母和 + / = 號(hào)進(jìn)行表示,這些都是可以被正常顯示的 ascii 字符,即“安全”的字節(jié)。絕大部分的計(jì)算機(jī)和操作系統(tǒng)都對(duì) ascii 有著良好的支持,保證了編碼之后的字節(jié)流能被正確地復(fù)制、傳播、解析。

          注:下文關(guān)于字節(jié)順序內(nèi)容均基于機(jī)器采用小端模式的前提進(jìn)行討論(關(guān)于大小端字節(jié)序,可以閱讀《面試必考,史上最通俗大小端字節(jié)序詳解》)。

          5、什么是Base 128

          Base 64 存在的問題就是:編碼后的每一個(gè)字節(jié)的最高兩位總是 0,在不考慮 pad 的情況下,有效 bit 只占 bit 總數(shù)的 75%,造成大量的空間浪費(fèi)。

          是否可以進(jìn)一步提高信息密度呢?

          意識(shí)到這一點(diǎn),你就很自然能想象出 Base 128 的大致實(shí)現(xiàn)思路了:將字節(jié)流按 7 bits 進(jìn)行分組,然后低位補(bǔ) 0。

          但問題來了:Base 64 實(shí)際上用了 64+1 個(gè) ascii 字符,按照這個(gè)思路 Base 128 需要使用 128+1 個(gè) ascii 個(gè)字符,但是 ascii 字符一共只有 128 個(gè)。

          另外:即使不考慮 pad,ascii 中包含了一些不可以正常打印的控制字符,編碼之后的字符還可能包含會(huì)被不同操作系統(tǒng)轉(zhuǎn)換的換行符號(hào)(10 和 13)。因此,Base 64 至今依然沒有被 Base 128 替代。

          Base 64 的規(guī)則因?yàn)樯鲜鱿拗撇荒芡昝赖財(cái)U(kuò)展到 Base 128,所以現(xiàn)有基于 Base 64 擴(kuò)展而來的編碼方式大部分都屬于變種:如 LEB128(Little-Endian Base 128)、 Base 85 (Ascii 85),以及本文的主角:Base 128 Varints。

          6、什么是Base 128 Varints

          6.1 基本概念

          Base 128 Varints 是 Google 開發(fā)的序列化庫 Protocol Buffers 所用的編碼方式。

          以下為 Protobuf 官方文檔中對(duì)于 Varints 的解釋:

          Varints are a method of serializing integers using one or more bytes. Smaller numbers take a smaller number of bytes.

          即:使用一個(gè)或多個(gè)字節(jié)對(duì)整數(shù)進(jìn)行序列化,小的數(shù)字占用更少的字節(jié)。

          簡單來說,Base 128 Varints 編碼原理就是盡量只儲(chǔ)存整數(shù)的有效位,高位的 0 盡可能拋棄。

          Base 128 Varints 有兩個(gè)需要注意的細(xì)節(jié):

          • 1)只能對(duì)一部分?jǐn)?shù)據(jù)結(jié)構(gòu)進(jìn)行編碼,不適用于所有字節(jié)流(當(dāng)然你可以把任意字節(jié)流轉(zhuǎn)換為 string,但不是所有語言都支持這個(gè) trick)。否則無法識(shí)別哪部分是無效的 bits;
          • 2)編碼后的字節(jié)可以不存在于 Ascii 表中,因?yàn)楹?Base 64 使用場景不同,不用考慮是否能正常打印。

          下面以例子進(jìn)行說明 Base 128 Varints 的編碼實(shí)現(xiàn)。

          6.2 舉個(gè)例子

          對(duì)于Base 128 Varints 編碼后的每個(gè)字節(jié),低 7 位用于儲(chǔ)存數(shù)據(jù),最高位用來標(biāo)識(shí)當(dāng)前字節(jié)是否是當(dāng)前整數(shù)的最后一個(gè)字節(jié),稱為最高有效位(most significant bit, 簡稱msb)。msb 為 1 時(shí),代表著后面還有數(shù)據(jù);msb 為 0 時(shí)代表著當(dāng)前字節(jié)是當(dāng)前整數(shù)的最后一個(gè)字節(jié)。

          下面我們用實(shí)際的例子來更好的理解它。

          下圖是編碼后的整數(shù)1:1 只需要用一個(gè)字節(jié)就能表示完全,所以 msb 為 0。

          對(duì)于需要多個(gè)字節(jié)來儲(chǔ)存的數(shù)據(jù),如 300 (0b100101100),有效位數(shù)為 9,編碼后需要兩個(gè)字節(jié)儲(chǔ)存。

          下圖是編碼后的整數(shù)300:第一個(gè)字節(jié)的 msb 為 1,最后一個(gè)字節(jié)的 msb 為 0。

          要將這兩個(gè)字節(jié)解碼成整數(shù),需要三個(gè)步驟:

          • 1)去除 msb;
          • 2)將字節(jié)流逆序(msb 為 0 的字節(jié)儲(chǔ)存原始數(shù)據(jù)的高位部分,小端模式);
          • 3)最后拼接所有的 bits。

          6.3 對(duì)整數(shù)進(jìn)行編碼的例子

          下面這個(gè)例子展示如何將使用 Base 128 Varints 對(duì)整數(shù)進(jìn)行編碼。

          具體過程是:

          • 1)將數(shù)據(jù)按每 7 bits 一組拆分;
          • 2)逆序每一個(gè)組;
          • 3)添加 msb。

          需要注意的是:無論是編碼還是解碼,逆序字節(jié)流這一步在機(jī)器處理中實(shí)際是不存在的,機(jī)器采用小端模式處理數(shù)據(jù),此處逆序僅是為了符合人的閱讀習(xí)慣而寫出。

          下面展示 Go 版本的 protobuf 中關(guān)于 Base 128 Varints 的實(shí)現(xiàn):

          // google.golang.org/protobuf@v1.25.0/encoding/protowire/wire.go

           

          // AppendVarint appends v to b as a varint-encoded uint64.

          funcAppendVarint(b []byte, v uint64) []byte{

           switch{

           casev < 1<<7:

            b = append(b, byte(v))

           casev < 1<<14:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte(v>>7))

           casev < 1<<21:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte(v>>14))

           casev < 1<<28:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte(v>>21))

           casev < 1<<35:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte((v>>21)&0x7f|0x80),

             byte(v>>28))

           casev < 1<<42:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte((v>>21)&0x7f|0x80),

             byte((v>>28)&0x7f|0x80),

             byte(v>>35))

           casev < 1<<49:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte((v>>21)&0x7f|0x80),

             byte((v>>28)&0x7f|0x80),

             byte((v>>35)&0x7f|0x80),

             byte(v>>42))

           casev < 1<<56:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte((v>>21)&0x7f|0x80),

             byte((v>>28)&0x7f|0x80),

             byte((v>>35)&0x7f|0x80),

             byte((v>>42)&0x7f|0x80),

             byte(v>>49))

           casev < 1<<63:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte((v>>21)&0x7f|0x80),

             byte((v>>28)&0x7f|0x80),

             byte((v>>35)&0x7f|0x80),

             byte((v>>42)&0x7f|0x80),

             byte((v>>49)&0x7f|0x80),

             byte(v>>56))

           default:

            b = append(b,

             byte((v>>0)&0x7f|0x80),

             byte((v>>7)&0x7f|0x80),

             byte((v>>14)&0x7f|0x80),

             byte((v>>21)&0x7f|0x80),

             byte((v>>28)&0x7f|0x80),

             byte((v>>35)&0x7f|0x80),

             byte((v>>42)&0x7f|0x80),

             byte((v>>49)&0x7f|0x80),

             byte((v>>56)&0x7f|0x80),

             1)

           }

           returnb

          }

          從源碼中可以看出:protobuf 的 varints 最多可以編碼 8 字節(jié)的數(shù)據(jù),這是因?yàn)榻^大部分的現(xiàn)代計(jì)算機(jī)最高支持處理 64 位的整型。

          7、Protobuf支持的數(shù)據(jù)類型

          7.1 概述

          Protobuf 不僅支持整數(shù)類型,下圖列出 protobuf 支持的數(shù)據(jù)類型(wire type)。

          在上一小節(jié)中展示的編碼與解碼的例子中的“整數(shù)”并不是我們一般理解的整數(shù)(編程語言中的 int32,uint32 等),而是對(duì)應(yīng)著上圖中的 Varint。

          當(dāng)實(shí)際編程中使用 protobuf 進(jìn)行編碼時(shí)經(jīng)過了兩步處理:

          • 1)將編程語言的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化為 wire type;
          • 2)根據(jù)不同的 wire type 使用對(duì)應(yīng)的方法編碼(前文所提到的 Base 128 Varints 用來編碼 varint 類型的數(shù)據(jù),其他 wire type 則使用其他編碼方式)。

          {obj}  -> {wire type} -> {encoded bytestream}

          uint32-> wire type0 -> varint

          int32-> wire type0 -> varint

          bool-> wire type0 -> varint

          string-> wire type2 -> length-delimited

          ...

          不同語言中 wire type 實(shí)際上也可能采用了語言中的某種類型來儲(chǔ)存 wire type 的數(shù)據(jù)。例如 Go 中使用了 uint64 來儲(chǔ)存 wire type 0。

          一般來說,大多數(shù)語言中的無符號(hào)整型被轉(zhuǎn)換為 varints 之后,有效位上的內(nèi)容并沒有改變。

          下面說明部分其他數(shù)據(jù)類型到 wire type 的轉(zhuǎn)換規(guī)則。

          7.2 有符號(hào)整型

          Protobuf中有符號(hào)整型采用 ZigZag 編碼來將 sint32 和 sint64 轉(zhuǎn)換為 wire type 0。

          下面是 ZigZag 編碼的規(guī)則(注意是算術(shù)位移):

          (n << 1) ^ (n >> 31)  // for 32-bit signed integer

          (n << 1) ^ (n >> 63)  // for 64-bit signed integer

          或者從數(shù)學(xué)意義來理解:

          n * 2       // when n >= 0

          -n * 2 - 1  // when n < 0

          下圖展示了部分 ZigZag 編碼的例子:

          如果不先采用 ZigZag 編碼成 wire type,負(fù)值 sint64 直接使用 Base 128 Varints 編碼之后的長度始終為ceil(64/7)=10bytes,浪費(fèi)大量空間。

          使用 ZigZag 編碼后,絕對(duì)值較小的負(fù)數(shù)的長度能夠被顯著壓縮:

          對(duì)于 -234(sint32) 這個(gè)例子,編碼成 varints 之前采用 ZigZag 編碼,比直接編碼成 varints 少用了 60%的空間。

          當(dāng)然,ZigZag 編碼也不是完美的方法。當(dāng)你嘗試把 sint32 或 sint64 范圍內(nèi)所有的整數(shù)都編碼成 varints 字節(jié)流,使用 ZigZag 已經(jīng)不能壓縮字節(jié)數(shù)量了。

          ZigZag 雖然能壓縮部分負(fù)數(shù)的空間,但同時(shí)正數(shù)變得需要更多的空間來儲(chǔ)存。

          因此,建議在業(yè)務(wù)場景允許的場景下盡量用無符號(hào)整型,有助于進(jìn)一步壓縮編碼后的空間。

          7.3 定長數(shù)據(jù)(64-bit)

          Protobuf中定長數(shù)據(jù)直接采用小端模式儲(chǔ)存,不作轉(zhuǎn)換。

          7.4 字符串

          以字符串"testing"為例:

          Protobuf編碼后的 value 分為兩部分:

          • 1)藍(lán)色:表示字符串采用 UTF-8 編碼后字節(jié)流的長度(bytes),采用 Base 128 Varints 進(jìn)行編碼;
          • 2)白色:字符串用 UTF-8 編碼后的字節(jié)流。

          8、Protobuf的消息結(jié)構(gòu)

          Protobuf 采用 proto3 作為 DSL 來描述其支持的消息結(jié)構(gòu)。

          就像下面這樣:

          syntax = "proto3";

           

          message SearchRequest {

            stringquery = 1;

            int32page_number = 2;

            int32result_per_page = 3;

          }

          設(shè)想一下這樣的場景:數(shù)據(jù)的發(fā)送方在業(yè)務(wù)迭代之后需要在消息內(nèi)攜帶更多的字段,而有的接收方并沒有更新自己的 proto 文件。要保持較好的兼容性,接收方分辨出哪些字段是自己可以識(shí)別的,哪些是不能識(shí)別的新增字段。要做到這一點(diǎn),發(fā)送方在編碼消息時(shí)還必須附帶每個(gè)字段的 key,客戶端讀取到未知的 key 時(shí),可以直接跳過對(duì)應(yīng)的 value。

          proto3 中每一個(gè)字段后面都有一個(gè) = x,比如:

          stringquery = 1;

          這里的等號(hào)并不是用于賦值,而是給每一個(gè)字段指定一個(gè) ID,稱為 field number。消息內(nèi)同一層次字段的 field number 必須各不相同。

          上面所說的 key,在 protobuf 源碼中被稱為 tag。

          tag 由 field number 和 type 兩部分組成:

          • 1)field number 左移 3 bits;
          • 2)在最低 3 bits 寫入 wire type。

          下面展示一個(gè)生成 tag 例子:

          Go 版本 Protobuf 中生成 tag 的源碼:

          // google.golang.org/protobuf@v1.25.0/encoding/protowire/wire.go

           

          // EncodeTag encodes the field Number and wire Type into its unified form.

          funcEncodeTag(num Number, typ Type) uint64{

              returnuint64(num)<<3 | uint64(typ&7)

          }

          源碼中生成的 tag 是 uint64,代表著 field number 可以使用 61 個(gè) bit 嗎?

          并非如此!

          事實(shí)上:tag 的長度不能超過 32 bits,意味著 field number 的最大取值為 2^29-1 (536870911)。

          而且在這個(gè)范圍內(nèi),有一些數(shù)是不能被使用的:

          • 1)0 :protobuf 規(guī)定 field number 必須為正整數(shù);
          • 2)19000 到 19999: protobuf 僅供內(nèi)部使用的保留位。

          理解了生成 tag 的規(guī)則之后,不難得出以下結(jié)論:

          • 1)field number 不必從 1 開始,可以從合法范圍內(nèi)的任意數(shù)字開始;
          • 2)不同字段間的 field number 不必連續(xù),只要合法且不同即可。

          但是實(shí)際上:大多數(shù)人分配 field number 還是會(huì)從 1 開始,因?yàn)?tag 最終要經(jīng)過 Base 128 Varints 編碼,較小的 field number 有助于壓縮空間,field number 為 1 到 15 的 tag 最終僅需占用一個(gè)字節(jié)。

          當(dāng)你的 message 有超過 15 個(gè)字段時(shí),Google 也不建議你將 1 到 15 立馬用完。如果你的業(yè)務(wù)日后有新增字段的可能,并且新增的字段使用比較頻繁,你應(yīng)該在 1 到 15 內(nèi)預(yù)留一部分供新增的字段使用。

          當(dāng)你修改的 proto 文件需要注意:

          • 1)field number 一旦被分配了就不應(yīng)該被更改,除非你能保證所有的接收方都能更新到最新的 proto 文件;
          • 2)由于 tag 中不攜帶 field name 信息,更改 field name 并不會(huì)改變消息的結(jié)構(gòu)。

          發(fā)送方認(rèn)為的 apple 到接受方可能會(huì)被識(shí)別成 pear。雙方把字段讀取成哪個(gè)名字完全由雙方自己的 proto 文件決定,只要字段的 wire type 和 field number 相同即可。

          由于 tag 中攜帶的類型是 wire type,不是語言中具體的某個(gè)數(shù)據(jù)結(jié)構(gòu),而同一個(gè) wire type 可以被解碼成多種數(shù)據(jù)結(jié)構(gòu),具體解碼成哪一種是根據(jù)接收方自己的 proto 文件定義的。

          修改 proto 文件中的類型,有可能導(dǎo)致錯(cuò)誤:

          最后用一個(gè)比前面復(fù)雜一點(diǎn)的例子來結(jié)束本節(jié)內(nèi)容:

          9、Protobuf中的嵌套消息

          嵌套消息的實(shí)現(xiàn)并不復(fù)雜。

          在上一節(jié)展示的 protobuf 的 wire type 中,wire type2 (length-delimited)不僅支持 string,也支持 embedded messages。

          對(duì)于嵌套消息:首先你要將被嵌套的消息進(jìn)行編碼成字節(jié)流,然后你就可以像處理 UTF-8 編碼的字符串一樣處理這些字節(jié)流:在字節(jié)流前面加入使用 Base 128 Varints 編碼的長度即可。

          10、Protobuf中重復(fù)消息的編碼規(guī)則

          假設(shè)接收方的 proto3 中定義了某個(gè)字段(假設(shè) field number=1),當(dāng)接收方從字節(jié)流中讀取到多個(gè) field number=1 的字段時(shí),會(huì)執(zhí)行 merge 操作。

          merge 的規(guī)則如下:

          • 1)如果字段為不可分割的類型,則直接覆蓋;
          • 2)如果字段為 repeated,則 append 到已有字段;
          • 3)如果字段為嵌套消息,則遞歸執(zhí)行 merge;

          如果字段的 field number 相同但是結(jié)構(gòu)不同,則出現(xiàn) error。

          以下為 Go 版本 Protobuf 中 merge 的部分:

          // google.golang.org/protobuf@v1.25.0/proto/merge.go

           

          // Merge merges src into dst, which must be a message with the same descriptor.

          //

          // Populated scalar fields in src are copied to dst, while populated

          // singular messages in src are merged into dst by recursively calling Merge.

          // The elements of every list field in src is appended to the corresponded

          // list fields in dst. The entries of every map field in src is copied into

          // the corresponding map field in dst, possibly replacing existing entries.

          // The unknown fields of src are appended to the unknown fields of dst.

          //

          // It is semantically equivalent to unmarshaling the encoded form of src

          // into dst with the UnmarshalOptions.Merge option specified.

          funcMerge(dst, src Message) {

           // TODO: Should nil src be treated as semantically equivalent to a

           // untyped, read-only, empty message? What about a nil dst?

           

           dstMsg, srcMsg := dst.ProtoReflect(), src.ProtoReflect()

           ifdstMsg.Descriptor() != srcMsg.Descriptor() {

            ifgot, want := dstMsg.Descriptor().FullName(), srcMsg.Descriptor().FullName(); got != want {

             panic(fmt.Sprintf("descriptor mismatch: %v != %v", got, want))

            }

            panic("descriptor mismatch")

           }

           mergeOptions{}.mergeMessage(dstMsg, srcMsg)

          }

           

          func(o mergeOptions) mergeMessage(dst, src protoreflect.Message) {

           methods := protoMethods(dst)

           ifmethods != nil&& methods.Merge != nil{

            in := protoiface.MergeInput{

             Destination: dst,

             Source:      src,

            }

            out := methods.Merge(in)

            ifout.Flags&protoiface.MergeComplete != 0 {

             return

            }

           }

           

           if!dst.IsValid() {

            panic(fmt.Sprintf("cannot merge into invalid %v message", dst.Descriptor().FullName()))

           }

           

           src.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool{

            switch{

            casefd.IsList():

             o.mergeList(dst.Mutable(fd).List(), v.List(), fd)

            casefd.IsMap():

             o.mergeMap(dst.Mutable(fd).Map(), v.Map(), fd.MapValue())

            casefd.Message() != nil:

             o.mergeMessage(dst.Mutable(fd).Message(), v.Message())

            casefd.Kind() == protoreflect.BytesKind:

             dst.Set(fd, o.cloneBytes(v))

            default:

             dst.Set(fd, v)

            }

            returntrue

           })

           

           iflen(src.GetUnknown()) > 0 {

            dst.SetUnknown(append(dst.GetUnknown(), src.GetUnknown()...))

           }

          }

          11、Protobuf的字段順序

          11.1 編碼結(jié)果與字段順序無關(guān)

          Proto 文件中定義字段的順序與最終編碼結(jié)果的字段順序無關(guān),兩者有可能相同也可能不同。

          當(dāng)消息被編碼時(shí),Protobuf 無法保證消息的順序,消息的順序可能隨著版本或者不同的實(shí)現(xiàn)而變化。任何 Protobuf 的實(shí)現(xiàn)都應(yīng)該保證字段以任意順序編碼的結(jié)果都能被讀取。

          以下是使用Protobuf時(shí)的一些常識(shí):

          • 1)序列化后的消息字段順序是不穩(wěn)定的;
          • 2)對(duì)同一段字節(jié)流進(jìn)行解碼,不同實(shí)現(xiàn)或版本的 Protobuf 解碼得到的結(jié)果不一定完全相同(bytes 層面),只能保證相同版本相同實(shí)現(xiàn)的 Protobuf 對(duì)同一段字節(jié)流多次解碼得到的結(jié)果相同;
          • 3)假設(shè)有一條消息foo,有幾種關(guān)系可能是不成立的(下方會(huì)接著詳細(xì)說明)。

          針對(duì)上述第 3)點(diǎn),這幾種關(guān)系可能是不成立的:

          foo.SerializeAsString() == foo.SerializeAsString()

          Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())

          CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())

          FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())

          11.2 相等消息編碼后結(jié)果可能不同

          假設(shè)有兩條邏輯上相等的消息,但是序列化之后的內(nèi)容(bytes 層面)不相同,原因有很多種可能。

          比如下面這些原因:

          • 1)其中一條消息可能使用了較老版本的 protobuf,不能處理某些類型的字段,設(shè)為 unknwon;
          • 2)使用了不同語言實(shí)現(xiàn)的 Protobuf,并且以不同的順序編碼字段;
          • 3)消息中的字段使用了不穩(wěn)定的算法進(jìn)行序列化;
          • 4)某條消息中有 bytes 類型的字段,用于儲(chǔ)存另一條消息使用 Protobuf 序列化的結(jié)果,而這個(gè) bytes 使用了不同的 Protobuf 進(jìn)行序列化;
          • 5)使用了新版本的 Protobuf,序列化實(shí)現(xiàn)不同;
          • 6)消息字段順序不同。

          12、參考資料

          [1] Protobuf官方編碼資料

          [2] Protobuf官方手冊

          [3] Why do we use Base64?

          [4] The Base16, Base32, and Base64 Data Encodings

          [5]Protobuf從入門到精通,一篇就夠!

          [6]如何選擇即時(shí)通訊應(yīng)用的數(shù)據(jù)傳輸格式

          [7]強(qiáng)列建議將Protobuf作為你的即時(shí)通訊應(yīng)用數(shù)據(jù)傳輸格式

          [8]APP與后臺(tái)通信數(shù)據(jù)格式的演進(jìn):從文本協(xié)議到二進(jìn)制協(xié)議

          [9]面試必考,史上最通俗大小端字節(jié)序詳解

          [10]移動(dòng)端IM開發(fā)需要面對(duì)的技術(shù)問題(含通信協(xié)議選擇)

          [11]簡述移動(dòng)端IM開發(fā)的那些坑:架構(gòu)設(shè)計(jì)、通信協(xié)議和客戶端

          [12]理論聯(lián)系實(shí)際:一套典型的IM通信協(xié)議設(shè)計(jì)詳解

          [13]58到家實(shí)時(shí)消息系統(tǒng)的協(xié)議設(shè)計(jì)等技術(shù)實(shí)踐分享

          (本文已同步發(fā)布于:http://www.52im.net/thread-4093-1-1.html



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 秦安县| 景德镇市| 舒城县| 施秉县| 修文县| 含山县| 阿克苏市| 离岛区| 新乡市| 武邑县| 阜阳市| 昆明市| 尉犁县| 太谷县| 宝应县| 抚顺县| 永济市| 吴江市| 文登市| 武义县| 孙吴县| 刚察县| 罗江县| 潢川县| 赣榆县| 贡觉县| 宁陵县| 香河县| 东源县| 招远市| 曲松县| 中方县| 阿克| 新乡市| 墨脱县| 和林格尔县| 太原市| 交口县| 绍兴市| 黎川县| 沂南县|