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

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

IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)

2023-01-28 17:15 作者:nickkckckck  | 我要投稿

本文由金蝶隨手記技術(shù)團(tuán)隊(duì)丁同舟分享。

1、引言

跟移動端IM中追求數(shù)據(jù)傳輸效率、網(wǎng)絡(luò)流量消耗等需求一樣,隨手記客戶端與服務(wù)端交互的過程中,對部分?jǐn)?shù)據(jù)的傳輸大小和效率也有較高的要求,普通的數(shù)據(jù)格式如 JSON 或者 XML 已經(jīng)不能滿足,因此決定采用 Google 推出的 Protocol Buffers 以達(dá)到數(shù)據(jù)高效傳輸。

本文將基于隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐,分享了Protobuf的技術(shù)原理、上手實(shí)戰(zhàn)等(本篇要分享的是技術(shù)原理),希望對你有用。

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

- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》

- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)

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

2、系列文章

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

  • 《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快幾倍?全方位實(shí)測!》

  • 《IM通訊協(xié)議專題學(xué)習(xí)(六):手把手教你如何在Android上從零使用Protobuf》(稍后發(fā)布..)

  • 《IM通訊協(xié)議專題學(xué)習(xí)(七):手把手教你如何在NodeJS中從零使用Protobuf》

  • 《IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)》(* 本文

  • 《IM通訊協(xié)議專題學(xué)習(xí)(九):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(實(shí)戰(zhàn)篇) 》(稍后發(fā)布..)

3、基本介紹

Protocol buffers?為 Google 提出的一種跨平臺、多語言支持且開源的序列化數(shù)據(jù)格式。相對于類似的 XML 和 JSON,Protocol buffers 更為小巧、快速和簡單。其語法目前分為proto2和proto3兩種格式。

相對于傳統(tǒng)的 XML 和 JSON, Protocol buffers 的優(yōu)勢主要在于:更加小、更加快。

對于自定義的數(shù)據(jù)結(jié)構(gòu),Protobuf 可以通過生成器生成不同語言的源代碼文件,讀寫操作都非常方便。

假設(shè)現(xiàn)在有下面 JSON 格式的數(shù)據(jù):

{

"id":1,

"name":"jojo",

"email":"123@qq.com",

}

使用 JSON 進(jìn)行編碼,得出byte長度為43的的二進(jìn)制數(shù)據(jù):

7b226964 223a312c 226e616d 65223a22 6a6f6a6f 222c2265 6d61696c 223a2231 32334071 712e636f 6d227d

如果使用 Protobuf 進(jìn)行編碼,得到的二進(jìn)制數(shù)據(jù)僅有20個字節(jié):

0a046a6f 6a6f1001 1a0a3132 33407171 2e636f6d

4、編碼原理

相對于基于純文本的數(shù)據(jù)結(jié)構(gòu)如 JSON、XML等,Protobuf 能夠達(dá)到小巧、快速的最大原因在于其獨(dú)特的編碼方式?!禤rotobuf從入門到精通,一篇就夠!》對 Protobuf 的 Encoding 作了很好的解析。

例如:對于int32類型的數(shù)字,如果很小的話,protubuf 因?yàn)椴捎昧薞arint方式,可以只用 1 個字節(jié)表示。

5、Varint原理

Varint 中每個字節(jié)的最高位 bit 表示此 byte 是否為最后一個 byte 。1 表示后續(xù)的 byte 也表示該數(shù)字,0 表示此 byte 為結(jié)束的 byte。

例如數(shù)字 300 用 Varint 表示為 1010 1100 0000 0010:

▲ 圖片源自《Protobuf從入門到精通,一篇就夠!》

注意:需要注意解析的時候會首先將兩個 byte 位置互換,因?yàn)樽止?jié)序采用了 little-endian 方式。

但 Varint 方式對于帶符號數(shù)的編碼效果比較差。因?yàn)閹Х枖?shù)通常在最高位表示符號,那么使用 Varint 表示一個帶符號數(shù)無論大小就必須要 5 個 byte(最高位的符號位無法忽略,因此對于 -1 的 Varint 表示就變成了 010001)。

Protobuf 引入了 ZigZag 編碼很好地解決了這個問題。

6、ZigZag編碼

關(guān)于 ZigZag 的編碼方式,博客園上的一篇博文《整數(shù)壓縮編碼 ZigZag》做出了詳細(xì)的解釋。

?

ZigZag 編碼按照數(shù)字的絕對值進(jìn)行升序排序,將整數(shù)通過一個 hash 函數(shù)h(n) = (n<<1)^(n>>31)(如果是 sint64 h(n) = (n<<1)^(n>>63))轉(zhuǎn)換為遞增的 32 位 bit 流。

關(guān)于為什么 64 的 ZigZag 為 80 01,《整數(shù)壓縮編碼 ZigZag》中有關(guān)于其編碼唯一可譯性的解釋。

通過 ZigZag 編碼,只要絕對值小的數(shù)字,都可以用較少位的 byte 表示。解決了負(fù)數(shù)的 Varint 位數(shù)會比較長的問題。

7、T-V and T-L-V

Protobuf 的消息結(jié)構(gòu)是一系列序列化后的Tag-Value對。其中 Tag 由數(shù)據(jù)的 field 和 writetype組成,Value 為源數(shù)據(jù)編碼后的二進(jìn)制數(shù)據(jù)。

假設(shè)有這樣一個消息:

message Person {

int32 id = 1;

string name = 2;

}

其中,id字段的field為1,writetype為int32類型對應(yīng)的序號。編碼后id對應(yīng)的 Tag 為?(field_number << 3) | wire_type = 0000 1000,其中低位的 3 位標(biāo)識 writetype,其他位標(biāo)識field。

每種類型的序號可以從這張表得到:

需要注意,對于string類型的數(shù)據(jù)(在上表中第三行),由于其長度是不定的,所以 T-V的消息結(jié)構(gòu)是不能滿足的,需要增加一個標(biāo)識長度的Length字段,即T-L-V結(jié)構(gòu)。

8、反射機(jī)制

Protobuf 本身具有很強(qiáng)的反射機(jī)制,可以通過 type name 構(gòu)造具體的 Message 對象。陳碩的文章《一種自動反射消息類型的 Google Protobuf 網(wǎng)絡(luò)傳輸方案》中對 GPB 的反射機(jī)制做了詳細(xì)的分析和源碼解讀。這里通過 protobuf-objectivec 版本的源碼,分析此版本的反射機(jī)制。

陳碩對 protobuf 的類結(jié)構(gòu)做出了詳細(xì)的分析 —— 其反射機(jī)制的關(guān)鍵類為Descriptor類:

每個具體 Message Type 對應(yīng)一個 Descriptor 對象。盡管我們沒有直接調(diào)用它的函數(shù),但是Descriptor在“根據(jù) type name 創(chuàng)建具體類型的 Message 對象”中扮演了重要的角色,起了橋梁作用。

同時,陳碩根據(jù) GPB 的 C++ 版本源代碼分析出其反射的具體機(jī)制:DescriptorPool類根據(jù) type name 拿到一個 Descriptor的對象指針,在通過MessageFactory工廠類根據(jù)Descriptor實(shí)例構(gòu)造出具體的Message對象。

示例代碼如下:

Message* createMessage(conststd::string& typeName)

{

??Message* message = NULL;

??constDescriptor* descriptor = DescriptorPool::generated_pool()->FindMessageTypeByName(typeName);

??if(descriptor)

??{

????constMessage* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);

????if(prototype)

????{

??????message = prototype->New();

????}

??}

??returnmessage;

}

注意:

  • 1)DescriptorPool 包含了程序編譯的時候所鏈接的全部 protobuf Message types;

  • 2)MessageFactory 能創(chuàng)建程序編譯的時候所鏈接的全部 protobuf Message types。

9、以Protobuf-objectivec為例

在 OC 環(huán)境下,假設(shè)有一份 Message 數(shù)據(jù)結(jié)構(gòu)如下:

message Person {

??string name = 1;

??int32 id = 2;

??string email = 3;

}

解碼此類型消息的二進(jìn)制數(shù)據(jù):

Person *newP = [[Person alloc] initWithData:data error:nil];

這里調(diào)用了:

- (instancetype)initWithData:(NSData*)data error:(NSError**)errorPtr {

????return[selfinitWithData:data extensionRegistry:nilerror:errorPtr];

}

其內(nèi)部調(diào)用了另一個構(gòu)造器:

- (instancetype)initWithData:(NSData *)data

???????????extensionRegistry:(GPBExtensionRegistry *)extensionRegistry

???????????????????????error:(NSError **)errorPtr {

??if((self = [self init])) {

????@try {

??????[self mergeFromData:data extensionRegistry:extensionRegistry];

??????????//...

????}

????@catch (NSException *exception) {

??????//...?

????}

??}

??return self;

}

去掉一些防御代碼和錯誤處理后,可以看到最終由mergeFromData:方法實(shí)現(xiàn)構(gòu)造:

- (void)mergeFromData:(NSData*)data extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {

??GPBCodedInputStream *input = [[GPBCodedInputStream alloc] initWithData:data]; //根據(jù)傳入的`data`構(gòu)造出數(shù)據(jù)流對象

??[selfmergeFromCodedInputStream:input extensionRegistry:extensionRegistry]; //通過數(shù)據(jù)流對象進(jìn)行merge

??[input checkLastTagWas:0]; //校檢

??[input release];

}

這個方法主要做了兩件事:

  • 1)通過傳入的 data 構(gòu)造GPBCodedInputStream對象實(shí)例;

  • 2)通過上面構(gòu)造的數(shù)據(jù)流對象進(jìn)行 merge 操作。

GPBCodedInputStream負(fù)責(zé)的工作很簡單,主要是把源數(shù)據(jù)緩存起來,并同時保存一系列的狀態(tài)信息,例如size, lastTag等。

其數(shù)據(jù)結(jié)構(gòu)非常簡單:

typedef struct GPBCodedInputStreamState {

constuint8_t *bytes;

size_t bufferSize;

size_t bufferPos;

?

// For parsing subsections of an input stream you can put a hard limit on

// how much should be read. Normally the limit is the end of the stream,

// but you can adjust it to anywhere, and if you hit it you will be at the

// end of the stream, until you adjust the limit.

size_t currentLimit;

int32_t lastTag;

NSUIntegerrecursionDepth;

} GPBCodedInputStreamState;

?

@interface GPBCodedInputStream () {

@package

struct GPBCodedInputStreamState state_;

NSData *buffer_;

}

merge 操作內(nèi)部實(shí)現(xiàn)比較復(fù)雜,首先會拿到一個當(dāng)前 Message 對象的 Descriptor 實(shí)例,這個 Descriptor 實(shí)例主要保存 Message 的源文件 Descriptor 和每個 field 的 Descriptor,然后通過循環(huán)的方式對 Message 的每個 field 進(jìn)行賦值。

Descriptor 簡化定義如下:

@interfaceGPBDescriptor : NSObject<NSCopying>

@property(nonatomic, readonly, strong, nullable) NSArray<GPBFieldDescriptor*> *fields;

@property(nonatomic, readonly, strong, nullable) NSArray<GPBOneofDescriptor*> *oneofs; //用于 repeated 類型的 filed

@property(nonatomic, readonly, assign) GPBFileDescriptor *file;

@end

其中GPBFieldDescriptor定義如下:

@interface GPBFieldDescriptor () {

@package

?GPBMessageFieldDescription *description_;

?GPB_UNSAFE_UNRETAINED GPBOneofDescriptor *containingOneof_;

?

?SELgetSel_;

?SELsetSel_;

?SELhasOrCountSel_;? // *Count for map<>/repeated fields, has* otherwise.

?SELsetHasSel_;

}

其中GPBMessageFieldDescription保存了 field 的各種信息,如數(shù)據(jù)類型、filed 類型、filed id等。除此之外,getSel和setSel為這個 field 在對應(yīng)類的屬性的 setter 和 getter 方法。

mergeFromCodedInputStream:方法的簡化版實(shí)現(xiàn)如下:

- (void)mergeFromCodedInputStream:(GPBCodedInputStream *)input

???????????????extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {

?GPBDescriptor *descriptor = [selfdescriptor]; //生成當(dāng)前 Message 的`Descriptor`實(shí)例

?GPBFileSyntax syntax = descriptor.file.syntax; //syntax 標(biāo)識.proto文件的語法版本 (proto2/proto3)

?NSUInteger startingIndex = 0; //當(dāng)前位置

?NSArray *fields = descriptor->fields_; //當(dāng)前 Message 的所有 fileds

?

?//循環(huán)解碼

?for(NSUIntegeri = 0; i < fields.count; ++i) {

??//拿到當(dāng)前位置的`FieldDescriptor`

?????GPBFieldDescriptor *fieldDescriptor = fields[startingIndex];

?????//判斷當(dāng)前field的類型

?????GPBFieldType fieldType = fieldDescriptor.fieldType;

?????if(fieldType == GPBFieldTypeSingle) {

???????//`MergeSingleFieldFromCodedInputStream` 函數(shù)中解碼 Single 類型的 field 的數(shù)據(jù)

???????MergeSingleFieldFromCodedInputStream(self, fieldDescriptor, syntax, input, extensionRegistry);

???????//當(dāng)前位置+1

???????startingIndex += 1;

?????} else if(fieldType == GPBFieldTypeRepeated) {

????????// ...

???????// Repeated 解碼操作

?????} else{?

???????// ...

???????// 其他類型解碼操作

?????}

??}? // for(i < numFields)

}

可以看到,descriptor在這里是直接通過 Message 對象中的方法拿到的,而不是通過工廠構(gòu)造:

GPBDescriptor *descriptor = [self descriptor];

?

//`desciptor`方法定義

- (GPBDescriptor *)descriptor {

?return [[selfclass] descriptor];

}

這里的descriptor類方法實(shí)際上是由GPBMessage的子類具體實(shí)現(xiàn)的。

例如在Person這個消息結(jié)構(gòu)中,其descriptor方法定義如下:

+ (GPBDescriptor *)descriptor {

?static GPBDescriptor *descriptor = nil;

?if(!descriptor) {

???static GPBMessageFieldDescription fields[] = {

?????{

???????.name = "name",

???????.dataTypeSpecific.className = NULL,

???????.number = Person_FieldNumber_Name,

???????.hasIndex = 0,

???????.offset = (uint32_t)offsetof(Person__storage_, name),

???????.flags = GPBFieldOptional,

???????.dataType = GPBDataTypeString,

?????},

?????//...

?????//每個field都會在這里定義出`GPBMessageFieldDescription`

???};

???GPBDescriptor *localDescriptor = //這里會根據(jù)fileds和其他一系列參數(shù)構(gòu)造出一個`Descriptor`對象

???descriptor = localDescriptor;

?}

?return descriptor;

}

接下來,在構(gòu)造出 Message 的 Descriptor 后,會對所有的 fields 進(jìn)行遍歷解碼。解碼時會根據(jù)不同的fieldType調(diào)用不同的解碼函數(shù)。

例如對于fieldType == GPBFieldTypeSingle,會調(diào)用 Single 類型的解碼函數(shù):

MergeSingleFieldFromCodedInputStream(self, fieldDescriptor, syntax, input, extensionRegistry);

MergeSingleFieldFromCodedInputStream內(nèi)部提供了一系列宏定義,針對不同的數(shù)據(jù)類型進(jìn)行數(shù)據(jù)解碼。

#define CASE_SINGLE_POD(NAME, TYPE, FUNC_TYPE)???????????????????????????? \

???caseGPBDataType##NAME: {????????????????????????????????????????????? \

?????TYPE val = GPBCodedInputStreamRead##NAME(&input->state_);??????????? \

?????GPBSet##FUNC_TYPE##IvarWithFieldInternal(self, field, val, syntax);? \

?????break;?????????????????????????????????????????????????????????????? \

???????????}

#define CASE_SINGLE_OBJECT(NAME)?????????????????????????????????????????? \

???caseGPBDataType##NAME: {????????????????????????????????????????????? \

?????idval = GPBCodedInputStreamReadRetained##NAME(&input->state_);????? \

?????GPBSetRetainedObjectIvarWithFieldInternal(self, field, val, syntax); \

?????break;?????????????????????????????????????????????????????????????? \

???}

?

?????CASE_SINGLE_POD(Int32, int32_t, Int32)

??...

?

#undef CASE_SINGLE_POD

#undef CASE_SINGLE_OBJECT

例如:對于int32類型的數(shù)據(jù),最終會調(diào)用int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state);函數(shù)讀取數(shù)據(jù)并賦值。

這里內(nèi)部實(shí)現(xiàn)其實(shí)就是對于 Varint 編碼的解碼操作:

int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state) {

?int32_t value = ReadRawVarint32(state);

?return value;

}

在對數(shù)據(jù)解碼完成后,拿到一個int32_t,此時會調(diào)用GPBSetInt32IvarWithFieldInternal進(jìn)行賦值操作。

其簡化實(shí)現(xiàn)如下:

void GPBSetInt32IvarWithFieldInternal(GPBMessage *self,

?????????????????????????????????????GPBFieldDescriptor *field,

?????????????????????????????????????int32_t value,

?????????????????????????????????????GPBFileSyntax syntax) {

?

?//最終的賦值操作

?//此處`self`為`GPBMessage`實(shí)例

?uint8_t *storage = (uint8_t *)self->messageStorage_;

?int32_t *typePtr = (int32_t *)&storage[field->description_->offset];

?*typePtr = value;

?

}

其中typePtr為當(dāng)前需要賦值的變量的指針。至此,單個 field 的賦值操作已經(jīng)完成。

總結(jié)一下,在 protobuf-objectivec 版本中,反射機(jī)制中構(gòu)建 Message 對象的流程大致為:

  • 1)通過 Message 的具體子類構(gòu)造其 Descriptor,Descriptor 中包含了所有 field 的 FieldDescriptor;

  • 2)循環(huán)通過每個 FieldDescriptor 對當(dāng)前 Message 對象的指定 field 賦值。

10、參考資料

[1]?Protobuf 官方開發(fā)者指南(中文譯版)

[2]?Protobuf官方手冊

[3]?Why do we use Base64?

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

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

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

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

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

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

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

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

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

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

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


IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)的評論 (共 條)

分享到微博請遵守國家法律
柳江县| 鄂温| 泰兴市| 宁夏| 哈尔滨市| 化隆| 体育| 花垣县| 融水| 甘孜县| 大渡口区| 监利县| 商城县| 武陟县| 长泰县| 辽阳市| 烟台市| 繁峙县| 板桥市| 梁平县| 洞头县| 临夏市| 始兴县| 策勒县| 凌海市| 济阳县| 永济市| 尼木县| 政和县| 九龙城区| 广宁县| 依安县| 罗甸县| 长葛市| 棋牌| 雅安市| 安新县| 治多县| 庄河市| 勐海县| 天柱县|