最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

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

2022-12-02 12:29 作者:nickkckckck  | 我要投稿

本文由騰訊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)


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

分享到微博請(qǐng)遵守國家法律
商洛市| 西乌珠穆沁旗| 谷城县| 米易县| 方正县| 渭源县| 益阳市| 清水河县| 阿拉善右旗| 广元市| 天水市| 曲水县| 大同市| 巴中市| 永安市| 泽库县| 祁门县| 扶余县| 金昌市| 明水县| 米泉市| 玉林市| 乌海市| 宜宾县| 鸡西市| 哈密市| 滕州市| 吉木乃县| 鄂州市| 邻水| 阿拉善左旗| 孟州市| 银川市| 乐陵市| 拉孜县| 文水县| 宜章县| 通海县| 屏边| 萝北县| 潜江市|