<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Jack Jiang

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

    本文由金蝶隨手記技術團隊丁同舟分享。

    1、引言

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

    本文將基于隨手記團隊的Protobuf應用實踐,分享了Protobuf的技術原理、上手實戰等(本篇要分享的是技術原理),希望對你有用。

    學習交流:

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

    2、系列文章

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

    3、基本介紹

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

    相對于傳統的 XML 和 JSON, Protocol buffers 的優勢主要在于:更加小、更加快。

    對于自定義的數據結構,Protobuf 可以通過生成器生成不同語言的源代碼文件,讀寫操作都非常方便。

    假設現在有下面 JSON 格式的數據:

    {

    "id":1,

    "name":"jojo",

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

    }

    使用 JSON 進行編碼,得出byte長度為43的的二進制數據:

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

    如果使用 Protobuf 進行編碼,得到的二進制數據僅有20個字節:

    0a046a6f 6a6f1001 1a0a3132 33407171 2e636f6d

    4、編碼原理

    相對于基于純文本的數據結構如 JSON、XML等,Protobuf 能夠達到小巧、快速的最大原因在于其獨特的編碼方式。《Protobuf從入門到精通,一篇就夠!》對 Protobuf 的 Encoding 作了很好的解析。

    例如:對于int32類型的數字,如果很小的話,protubuf 因為采用了Varint方式,可以只用 1 個字節表示。

    5、Varint原理

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

    例如數字 300 用 Varint 表示為 1010 1100 0000 0010:

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

    注意:需要注意解析的時候會首先將兩個 byte 位置互換,因為字節序采用了 little-endian 方式。

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

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

    6、ZigZag編碼

    關于 ZigZag 的編碼方式,博客園上的一篇博文《整數壓縮編碼 ZigZag》做出了詳細的解釋。

     

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

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

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

    7、T-V and T-L-V

    Protobuf 的消息結構是一系列序列化后的Tag-Value對。其中 Tag 由數據的 field 和 writetype組成,Value 為源數據編碼后的二進制數據。

    假設有這樣一個消息:

    message Person {

    int32 id = 1;

    string name = 2;

    }

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

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

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

    8、反射機制

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

    陳碩對 protobuf 的類結構做出了詳細的分析 —— 其反射機制的關鍵類為Descriptor類:

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

    同時,陳碩根據 GPB 的 C++ 版本源代碼分析出其反射的具體機制:DescriptorPool類根據 type name 拿到一個 Descriptor的對象指針,在通過MessageFactory工廠類根據Descriptor實例構造出具體的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 能創建程序編譯的時候所鏈接的全部 protobuf Message types。

    9、以Protobuf-objectivec為例

    在 OC 環境下,假設有一份 Message 數據結構如下:

    message Person {

      string name = 1;

      int32 id = 2;

      string email = 3;

    }

    解碼此類型消息的二進制數據:

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

    這里調用了:

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

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

    }

    其內部調用了另一個構造器:

    - (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:方法實現構造:

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

      GPBCodedInputStream *input = [[GPBCodedInputStream alloc] initWithData:data]; //根據傳入的`data`構造出數據流對象

      [selfmergeFromCodedInputStream:input extensionRegistry:extensionRegistry]; //通過數據流對象進行merge

      [input checkLastTagWas:0]; //校檢

      [input release];

    }

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

    • 1)通過傳入的 data 構造GPBCodedInputStream對象實例;
    • 2)通過上面構造的數據流對象進行 merge 操作。

    GPBCodedInputStream負責的工作很簡單,主要是把源數據緩存起來,并同時保存一系列的狀態信息,例如size, lastTag等。

    其數據結構非常簡單:

    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 操作內部實現比較復雜,首先會拿到一個當前 Message 對象的 Descriptor 實例,這個 Descriptor 實例主要保存 Message 的源文件 Descriptor 和每個 field 的 Descriptor,然后通過循環的方式對 Message 的每個 field 進行賦值。

    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 的各種信息,如數據類型、filed 類型、filed id等。除此之外,getSel和setSel為這個 field 在對應類的屬性的 setter 和 getter 方法。

    mergeFromCodedInputStream:方法的簡化版實現如下:

    - (void)mergeFromCodedInputStream:(GPBCodedInputStream *)input

                   extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {

     GPBDescriptor *descriptor = [selfdescriptor]; //生成當前 Message 的`Descriptor`實例

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

     NSUInteger startingIndex = 0; //當前位置

     NSArray *fields = descriptor->fields_; //當前 Message 的所有 fileds

     

     //循環解碼

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

      //拿到當前位置的`FieldDescriptor`

         GPBFieldDescriptor *fieldDescriptor = fields[startingIndex];

         //判斷當前field的類型

         GPBFieldType fieldType = fieldDescriptor.fieldType;

         if(fieldType == GPBFieldTypeSingle) {

           //`MergeSingleFieldFromCodedInputStream` 函數中解碼 Single 類型的 field 的數據

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

           //當前位置+1

           startingIndex += 1;

         } else if(fieldType == GPBFieldTypeRepeated) {

            // ...

           // Repeated 解碼操作

         } else{ 

           // ...

           // 其他類型解碼操作

         }

      }  // for(i < numFields)

    }

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

    GPBDescriptor *descriptor = [self descriptor];

     

    //`desciptor`方法定義

    - (GPBDescriptor *)descriptor {

     return [[selfclass] descriptor];

    }

    這里的descriptor類方法實際上是由GPBMessage的子類具體實現的。

    例如在Person這個消息結構中,其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 = //這里會根據fileds和其他一系列參數構造出一個`Descriptor`對象

       descriptor = localDescriptor;

     }

     return descriptor;

    }

    接下來,在構造出 Message 的 Descriptor 后,會對所有的 fields 進行遍歷解碼。解碼時會根據不同的fieldType調用不同的解碼函數。

    例如對于fieldType == GPBFieldTypeSingle,會調用 Single 類型的解碼函數:

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

    MergeSingleFieldFromCodedInputStream內部提供了一系列宏定義,針對不同的數據類型進行數據解碼。

    #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類型的數據,最終會調用int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state);函數讀取數據并賦值。

    這里內部實現其實就是對于 Varint 編碼的解碼操作:

    int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state) {

     int32_t value = ReadRawVarint32(state);

     return value;

    }

    在對數據解碼完成后,拿到一個int32_t,此時會調用GPBSetInt32IvarWithFieldInternal進行賦值操作。

    其簡化實現如下:

    void GPBSetInt32IvarWithFieldInternal(GPBMessage *self,

                                         GPBFieldDescriptor *field,

                                         int32_t value,

                                         GPBFileSyntax syntax) {

     

     //最終的賦值操作

     //此處`self`為`GPBMessage`實例

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

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

     *typePtr = value;

     

    }

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

    總結一下,在 protobuf-objectivec 版本中,反射機制中構建 Message 對象的流程大致為:

    • 1)通過 Message 的具體子類構造其 Descriptor,Descriptor 中包含了所有 field 的 FieldDescriptor;
    • 2)循環通過每個 FieldDescriptor 對當前 Message 對象的指定 field 賦值。

    10、參考資料

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

    [2] Protobuf官方手冊

    [3] Why do we use Base64?

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

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

    [5] 如何選擇即時通訊應用的數據傳輸格式

    [7] 強列建議將Protobuf作為你的即時通訊應用數據傳輸格式

    [8] APP與后臺通信數據格式的演進:從文本協議到二進制協議

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

    [10] 移動端IM開發需要面對的技術問題(含通信協議選擇)

    [11] 簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端

    [12] 理論聯系實際:一套典型的IM通信協議設計詳解

    [13] 58到家實時消息系統的協議設計等技術實踐分享

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



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


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
    主站蜘蛛池模板: 免费无码又爽又刺激网站直播| 亚洲一区二区三区自拍公司| 日本免费一区二区久久人人澡| 亚洲精品无码专区| 337p日本欧洲亚洲大胆艺术| 在线亚洲人成电影网站色www| 国产午夜鲁丝片AV无码免费| 免费视频专区一国产盗摄| 男女午夜24式免费视频| 一级毛片免费在线| 污网站在线免费观看| 亚洲色欲色欱wwW在线| 亚洲w码欧洲s码免费| 亚洲首页在线观看| 久久久久无码精品亚洲日韩 | 中国china体内裑精亚洲日本| 亚洲一区综合在线播放| 久久久青草青青亚洲国产免观| 久久精品夜色噜噜亚洲A∨| 免费A级毛片在线播放不收费| 免费黄色大片网站| 免费观看理论片毛片| 免费理论片51人人看电影| 一区二区无码免费视频网站| 中文字幕乱码免费视频| 91网站免费观看| 无码精品A∨在线观看免费| 曰批全过程免费视频网址| 2021在线永久免费视频| 最近免费中文字幕大全高清大全1| 免费国产99久久久香蕉| 久久免费观看国产99精品| 麻豆精品不卡国产免费看| 高清一区二区三区免费视频| 久草免费福利资源站| 狼群影院在线观看免费观看直播| 67pao强力打造高清免费| 精品福利一区二区三区免费视频 | 亚洲精品第一国产综合境外资源| 一级毛片直播亚洲| 国内精品久久久久久久亚洲|