Jack Jiang

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

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

          1、引言

          跟移動(dòng)端IM中追求數(shù)據(jù)傳輸效率、網(wǎng)絡(luò)流量消耗等需求一樣,隨手記客戶(hù)端與服務(wù)端交互的過(guò)程中,對(duì)部分?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ù)原理),希望對(duì)你有用。

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

          - 移動(dòng)端IM開(kāi)發(fā)入門(mén)文章:《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM

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

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

          2、系列文章

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

          3、基本介紹

          Protocol buffers 為 Google 提出的一種跨平臺(tái)、多語(yǔ)言支持且開(kāi)源的序列化數(shù)據(jù)格式。相對(duì)于類(lèi)似的 XML 和 JSON,Protocol buffers 更為小巧、快速和簡(jiǎn)單。其語(yǔ)法目前分為proto2和proto3兩種格式。

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

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

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

          {

          "id":1,

          "name":"jojo",

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

          }

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

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

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

          0a046a6f 6a6f1001 1a0a3132 33407171 2e636f6d

          4、編碼原理

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

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

          5、Varint原理

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

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

          ▲ 圖片源自《Protobuf從入門(mén)到精通,一篇就夠!

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

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

          Protobuf 引入了 ZigZag 編碼很好地解決了這個(gè)問(wèn)題。

          6、ZigZag編碼

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

           

          ZigZag 編碼按照數(shù)字的絕對(duì)值進(jìn)行升序排序,將整數(shù)通過(guò)一個(gè) 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)于其編碼唯一可譯性的解釋。

          通過(guò) ZigZag 編碼,只要絕對(duì)值小的數(shù)字,都可以用較少位的 byte 表示。解決了負(fù)數(shù)的 Varint 位數(shù)會(huì)比較長(zhǎng)的問(wèn)題。

          7、T-V and T-L-V

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

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

          message Person {

          int32 id = 1;

          string name = 2;

          }

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

          每種類(lèi)型的序號(hào)可以從這張表得到:

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

          8、反射機(jī)制

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

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

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

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

          示例代碼如下:

          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 包含了程序編譯的時(shí)候所鏈接的全部 protobuf Message types;
          • 2)MessageFactory 能創(chuàng)建程序編譯的時(shí)候所鏈接的全部 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;

          }

          解碼此類(lèi)型消息的二進(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è)構(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;

          }

          去掉一些防御代碼和錯(cuò)誤處理后,可以看到最終由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ù)流對(duì)象

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

            [input checkLastTagWas:0]; //校檢

            [input release];

          }

          這個(gè)方法主要做了兩件事:

          • 1)通過(guò)傳入的 data 構(gòu)造GPBCodedInputStream對(duì)象實(shí)例;
          • 2)通過(guò)上面構(gòu)造的數(shù)據(jù)流對(duì)象進(jìn)行 merge 操作。

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

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

          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ù)雜,首先會(huì)拿到一個(gè)當(dāng)前 Message 對(duì)象的 Descriptor 實(shí)例,這個(gè) Descriptor 實(shí)例主要保存 Message 的源文件 Descriptor 和每個(gè) field 的 Descriptor,然后通過(guò)循環(huán)的方式對(duì) Message 的每個(gè) field 進(jìn)行賦值。

          Descriptor 簡(jiǎn)化定義如下:

          @interfaceGPBDescriptor : NSObject<NSCopying>

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

          @property(nonatomic, readonly, strong, nullable) NSArray<GPBOneofDescriptor*> *oneofs; //用于 repeated 類(lèi)型的 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ù)類(lèi)型、filed 類(lèi)型、filed id等。除此之外,getSel和setSel為這個(gè) field 在對(duì)應(yīng)類(lèi)的屬性的 setter 和 getter 方法。

          mergeFromCodedInputStream:方法的簡(jiǎn)化版實(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)識(shí).proto文件的語(yǔ)法版本 (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的類(lèi)型

               GPBFieldType fieldType = fieldDescriptor.fieldType;

               if(fieldType == GPBFieldTypeSingle) {

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

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

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

                 startingIndex += 1;

               } else if(fieldType == GPBFieldTypeRepeated) {

                  // ...

                 // Repeated 解碼操作

               } else{ 

                 // ...

                 // 其他類(lèi)型解碼操作

               }

            }  // for(i < numFields)

          }

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

          GPBDescriptor *descriptor = [self descriptor];

           

          //`desciptor`方法定義

          - (GPBDescriptor *)descriptor {

           return [[selfclass] descriptor];

          }

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

          例如在Person這個(gè)消息結(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,

               },

               //...

               //每個(gè)field都會(huì)在這里定義出`GPBMessageFieldDescription`

             };

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

             descriptor = localDescriptor;

           }

           return descriptor;

          }

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

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

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

          MergeSingleFieldFromCodedInputStream內(nèi)部提供了一系列宏定義,針對(duì)不同的數(shù)據(jù)類(lèi)型進(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

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

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

          int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state) {

           int32_t value = ReadRawVarint32(state);

           return value;

          }

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

          其簡(jiǎ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)前需要賦值的變量的指針。至此,單個(gè) field 的賦值操作已經(jīng)完成。

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

          • 1)通過(guò) Message 的具體子類(lèi)構(gòu)造其 Descriptor,Descriptor 中包含了所有 field 的 FieldDescriptor;
          • 2)循環(huán)通過(guò)每個(gè) FieldDescriptor 對(duì)當(dāng)前 Message 對(duì)象的指定 field 賦值。

          10、參考資料

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

          [2] Protobuf官方手冊(cè)

          [3] Why do we use Base64?

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

          [5] Protobuf從入門(mén)到精通,一篇就夠!

          [5] 如何選擇即時(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開(kāi)發(fā)需要面對(duì)的技術(shù)問(wèn)題(含通信協(xié)議選擇)

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

          [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-4114-1-1.html



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開(kāi)發(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 找到我)。


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


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 金秀| 布尔津县| 洪泽县| 桃园县| 柏乡县| 萝北县| 西畴县| 德庆县| 宣武区| 社会| 枣阳市| 徐水县| 惠安县| 新宾| 内乡县| 乌什县| 扬中市| 彰化县| 镶黄旗| 九江县| 惠来县| 冷水江市| 清远市| 汝州市| 商丘市| 余干县| 南靖县| 山西省| 鹤山市| 永川市| 大渡口区| 漳浦县| 松潘县| 南召县| 贵定县| 崇仁县| 纳雍县| 无极县| 延津县| 宁强县| 长海县|