IM通訊協(xié)議專題學(xué)習(xí)(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理

本文由騰訊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í)交流:
- 移動(dòng)端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)
(本文已同步發(fā)布于:http://www.52im.net/thread-4093-1-1.html)
2、系列文章
本文是系列文章中的第?4?篇,本系列總目錄如下:
《IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通,一篇就夠!》
《IM通訊協(xié)議專題學(xué)習(xí)(二):快速理解Protobuf的背景、原理、使用、優(yōu)缺點(diǎn)》
《IM通訊協(xié)議專題學(xué)習(xí)(三):由淺入深,從根上理解Protobuf的編解碼原理》
《IM通訊協(xié)議專題學(xué)習(xí)(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理》(*?本文)
《IM通訊協(xié)議專題學(xué)習(xí)(五):Protobuf到底比JSON快幾倍?請(qǐng)看全方位實(shí)測!》(稍后發(fā)布..)
《IM通訊協(xié)議專題學(xué)習(xí)(六):手把手教你如何在Android上從零使用Protobuf》(稍后發(fā)布..)
《IM通訊協(xié)議專題學(xué)習(xí)(七):手把手教你如何在NodeJS中從零使用Protobuf》(稍后發(fā)布..)
《IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇) 》(稍后發(fā)布..)
《IM通訊協(xié)議專題學(xué)習(xí)(九):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(實(shí)戰(zhàn)篇) 》(稍后發(fā)布..)
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)我們?cè)谟?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官方手冊(cè)
[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)