<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、引言

    本文將帶你一起初步認識Thrift的序列化協議,包括Binary協議、Compact協議(類似于Protobuf)、JSON協議,希望能為你的通信協議格式選型帶來參考。

     
     
    技術交流:

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

    2、系列文章

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

    IM通訊協議專題學習(一):Protobuf從入門到精通,一篇就夠!

    IM通訊協議專題學習(二):快速理解Protobuf的背景、原理、使用、優缺點

    IM通訊協議專題學習(三):由淺入深,從根上理解Protobuf的編解碼原理

    IM通訊協議專題學習(四):從Base64到Protobuf,詳解Protobuf的數據編碼原理

    IM通訊協議專題學習(五):Protobuf到底比JSON快幾倍?全方位實測!

    IM通訊協議專題學習(六):手把手教你如何在Android上從零使用Protobuf

    IM通訊協議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf

    IM通訊協議專題學習(八):金蝶隨手記團隊的Protobuf應用實踐(原理篇)

    IM通訊協議專題學習(九):手把手教你如何在iOS上從零使用Protobuf

    IM通訊協議專題學習(十):初識 Thrift 序列化協議》(* 本文)

    另外:如果您還打算系統地學習IM開發,建議閱讀《新手入門一篇就夠:從零開發移動端IM》。

    3、 概述

    Thrift 是 Facebook 開源的一個高性能,輕量級 RPC 服務框架,是一套全棧式的 RPC 解決方案,包含序列化與服務通信能力,并支持跨平臺/跨語言。

    Thrift整體架構如圖所示:

    Thrift 軟件棧定義清晰,各層的組件松耦合、可插拔,能夠根據業務場景靈活組合。

    如圖所示:

    Thrift 本身是一個比較大的話題,本篇文章不會涉及到Thrift的全部內容,只會涉及到其中的序列化協議。

    4、 Binary協議

    4.1消息格式

    這里通過一個示例對 Binary 消息格式進行直觀的展示。

    IDL 定義如下:

    //接口

    service SupService {

        SearchDepartmentByKeywordResponse SearchDepartmentByKeyword(

            1: SearchDepartmentByKeywordRequest request)

    }

     

    //請求

    struct SearchDepartmentByKeywordRequest {

        1: optional string Keyword

        2: optional i32 Limit     

        3: optional i32 Offset

    }

     

    //假設request的payload如下:

    {

        Keyword: "lark",

        Limit: 50,

        Offset: nil,       

    }

    4.2編碼簡圖

    4.3編碼具體內容

    抓包拿到編碼后的字節流(轉成了十進制,方便大家看)。

    /* 接口名長度 */         0   0   0    25

    /* 接口名 */            83  101  97  114  99  104  68  101  112  97  114  116

                           109  101  110  116  66  121  75  101  121  119  111

                           114  100

    /* 消息類型 */           1

    /* 消息序號 */           0   0   0   1

    /* keyword 字段類型 */   11

    /* keyword 字段ID*/     0   1

    /* keyword len */      0   0   0   4

    /* keyword value */    108   97   114   107

    /* limit 字段類型 */     8

    /* limit 字段ID*/       0   2

    /* limit value */      0   0   0   50

    /* 字段終止符 */         0

    4.4編碼含義

    1)消息頭:

    msg_type(消息類型),包含四種類型:

    • 1)Call:客戶端消息。調用遠程方法,并且期待對方發送響應;
    • 2)OneWay:客戶端消息。調用遠程方法,不期待響應;
    • 3)Reply:服務端消息。正常響應;
    • 4)Exception:服務端消息。異常響應。

    msg_seq_id消息序號):

    • 1)客戶端使用消息序號來處理響應的失序到達,實現請求和響應的匹配;
    • 2)服務端不需要檢查該序列號,也不能對序列號有任何的邏輯依賴,只需要響應的時候將其原樣返回即可。

    2)消息體:

    消息體分為兩種編碼模式:

    • 1)定長類型 -> T-V 模式,即:字段類型 + 字段序號 + 字段值;
    • 2)變長類型 -> T-L-V 模式,即:字段類型 + 字段序號 + 字段長度 + 字段值。

    具體是:

    • 1)field_type:字段類型,包括 String、I64、Struct、Stop 等;
    • 2)fied_id:字段序號,解碼時通過序號確定字段;
    • 3)len:字段長度,用于變長類型,如 String;
    • 4)value:字段值。

    字段類型有兩個作用:

    • 1)Stop 類型用于停止嵌套解析;
    • 2)非 Stop 類型用于 Skip(Skip 操作是跳過當前字段,會在「常見問題 - 兼容性」進行講解)。

    4.5數據格式

    定長數據類型:

    變長數據類型:

    5、Compact 協議

    5.1概述

    Compact 協議是二進制壓縮協議,在大部分字段的編碼方式上與 Binary 協議保持一致。

    區別在于整數類型(包括變長類型的長度)采用了先 zigzag 編碼 ,再 varint 壓縮編碼實現,最大化節省空間開銷。

    那么問題來了,varint 和 zigzag 是什么?

    5.2varint 編碼

    解決的問題:定長存儲的整數類型絕對值較小時空間浪費大。

    據統計,RPC 通信時大部分時候傳遞的整數值都很小,如果使用定長存儲會很浪費。

    舉個 🌰,對 i32 類型的 7 進行編碼,可以說前面 3 個字節都浪費了:

    00000000 00000000 00000000 00000111

     

    解決思路:將整數類型由定長存儲轉為變長存儲(能用 1 個字節存下就堅決不用 2 個字節)

    原理并不復雜,就是將整數按 7bit 分段,每個字節的最高位作為標識位,標識后一個字節是否屬于該數據。1 代表后面的字節還是屬于當前數據,0 代表這是當前數據的最后一個字節。

    以 i32 類型,數值 955 為例,可以看出,由原來的 4 字節壓縮到了 2 字節:

    binary編碼:       00000000  00000000  00000011  10111011

    切分:        0000  0000000   0000000   0000111   0111011

    compact編碼:                          00000111  10111011

    當然,varint 編碼同樣存在缺陷,那就是存儲大數的時候,反而會比 binary 的空間開銷更大:本來 4 個字節存下的數可能需要 5 個字節,8 個字節存下的數可能需要 10 個字節。

    5.3zigzag 編碼

    解決的問題:絕對值較小的負數經過 varint 編碼后空間開銷較大 舉個 🌰,i32 類型的負數(-11)

     

    原碼:         10000000  00000000  00000000  00001011

    反碼:         11111111  11111111  11111111  11110100

    補碼:         11111111  11111111  11111111  11110101

    varint編碼:   00001111  11111111  11111111  11111111  11110101

    顯然,對于絕對值較小的負數,用 varint 編碼以后前導 1 過多,難以壓縮,空間開銷比 binary 編碼還大。

    解決思路:負數轉正數,從而把前導 1 轉成前導 0,便于 varint 壓縮

    算法公式 & 步驟 & 示范:

    //算法公式

    32位: (n << 1) ^ (n >> 31)

    64位: (n << 1) ^ (n >> 63)

     

    /*

     * 算法步驟:

     * 1. 不分正負:符號位后置,數值位前移

     * 2. 對于負數:符號位不變,數值位取反

     */

     

    //示例

    負數(-11)

      補碼:                     11111111  11111111  11111111  11110101

      符號位后置,數值位前移:      11111111  11111111  11111111  11101011

      符號位不變,數值位取反(21):  00000000  00000000  00000000  00010101

     

    正數(11)

      補碼:                     00000000  00000000  00000000  00010101

      符號位后置,數值位前移(22):  00000000  00000000  00000000  00101010

    奇怪的知識:為什么取名叫 zigzag?

    因為這個算法將負數編碼成正奇數,正數編碼成偶數。最后效果是正負數穿插向前。

    就像這樣:

    編碼前       編碼后

      0           0

      -1          1

      1           2

      -2          3

      2           4

    6、Json 協議

    Thrift 不僅支持二進制序列化協議,也支持 Json 這種文本協議。

    數據格式:

    /* bool、i8、i16、i32、i64、double、string */

    "編號": {

      "類型": "值"

    }

    //示例

    "1": {

      "str": "keyword"

    }

     

     

    /* struct */

    "編號": {

      "rec": {

        "成員編號": {

          "成員類型": "成員值"

        },

        ...

      }

    }

    //示例

    "1": {

      "rec": {

        "1": {

          "i32": 50

        }

      }

    }

     

     

    /* map */

    "編號": {

      "map": [

        "鍵類型",

        "值類型",

        元素個數,

          "鍵1",

          "值1",

          ...

          "鍵n",

          "值n"

       ]

    }

    //示例

    "6": {

      "map": [

        "i64",

        "str",

        1,

        666,

        "mapValue"

      ]

    }

     

    /* List */

    "編號": {

      "set/lst": [

        "值類型",

        元素個數,

        "ele1",

        "ele2",

        "elen"

      ]

    }

    //示例

    "2": {

      "lst": [

        "str",

        2,

        "lark","keyword"]

    }

    7、修改字段類型導致協議解析不一致的通信問題

    現象:A 服務訪問 B 服務,業務邏輯短時間處理完,但整個請求 15s 超時,必現。

    直接原因:IDL 類型被修改;并且只升級了服務端(B 服務),沒升級客戶端(A 服務)。

    本質原因:string 是變長編碼,i64 是定長編碼。由于客戶端沒有升級,所以反序列化的時候,會把 signTime 當做 string 類型來解析。而變長編碼是 T-L-V 模式,所以解析的時候會把 signTime 的低位 4 字節翻譯成 string 的 length。

    signTime 是時間戳,大整數,比如:1624206147902,轉成二進制為:

    100000000 00000000 00000001 01111010 00101010 00111011 00000001 00111110

    低位 4 字節轉成十進制為:378 。

    也就是要再讀 378 個字節作為 SignTime 的值,這已經超過了整個 payload 的大小,最終導致 Socket 讀超時。

    注:修改類型不一定就會導致超時,如果 value 的值比較小,解析到的 length 也比較小,能夠保證讀完。

    但是錯誤的解析可能會導致各種預期之外的情況,包括:

    • 1)亂碼;
    • 2)空值;
    • 3)報錯:unknown data type xxx (skip 異常)。

    8、通信協議帶來的常見問題

    8.1兼容性

    1)增加字段:

    通過 skip 來跳過增加的字段,從而保證兼容性。

    2)刪除字段:

    編譯生成的解析代碼是基于 field_id 的 switch-case 結構,語法結構上直接具備兼容性。

    3)修改字段名:

    不破壞兼容性,因為 binary 協議不會對 name 進行編碼。

    8.2Exception

    Thrift 有兩種 Exception:

    • 1)一種是框架內置的異常;
    • 2)一種是 IDL 自定義的異常。

    框架內置的異常包括:

    • 1)方法名錯誤;
    • 2)消息序列號錯誤;
    • 3)協議錯誤。

    這些異常由框架捕獲并封裝成 Exception 消息,反序列化時會轉成 error 并拋給上層。

    邏輯如下:

    另一種異常是由用戶在 IDL 中自定義的,關鍵字是 exception,用法上跟 struct 沒有太大區別。

    8.3optional、require 實現原理

    optional 表示字段可填,require 表示必填。

    字段被標識為 optional 之后:

    • 1)基本類型會被編譯為指針類型;
    • 2)序列化代碼會做空值判斷,如果字段為空,則不會被編碼。

    字段被標識為 require 之后:

    • 1)基本類型會被編譯為非指針類型(復合類型 optional 和 require 沒區別);
    • 2)序列化不會做空值判斷,字段一定會被編碼。如果沒有顯式賦值,就編碼默認值(默認空值,或者 IDL 顯式指定的默認值)。

    9、參考資料

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

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

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

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

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

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

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

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

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

    [10] 金蝶隨手記團隊的Protobuf應用實踐(原理篇)

    [11] 新手入門一篇就夠:從零開發移動端IM


    (本文已同步發布于:http://www.52im.net/thread-4576-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
    主站蜘蛛池模板: 亚洲欧美自偷自拍另类视| 国产无限免费观看黄网站| 免费一级国产生活片| 国产免费区在线观看十分钟| 亚洲国产精品高清久久久| 日韩版码免费福利视频| 国产精品久久亚洲一区二区| 亚洲色大成网站WWW久久九九| 99久久99久久免费精品小说| 亚洲乱码av中文一区二区| 国产亚洲情侣一区二区无| 亚洲视频在线免费观看| 亚洲美国产亚洲AV| 亚洲国产日韩在线视频| 久久电影网午夜鲁丝片免费| 黄色网页在线免费观看| 亚洲人成人77777在线播放| 成人亚洲网站www在线观看| 亚洲成人免费网站| fc2免费人成为视频| 亚洲sss综合天堂久久久| 亚洲无人区一区二区三区| 妞干网免费视频在线观看| 无码国产精品一区二区免费16| 亚洲jizzjizz少妇| 在线电影你懂的亚洲| 亚洲国模精品一区| 成人免费一区二区三区在线观看| 97在线免费观看视频| 国产成人综合久久精品亚洲| 91亚洲自偷手机在线观看| 中文字幕亚洲一区二区va在线| 思思99re66在线精品免费观看| 久久久久国产精品免费网站| 免费看一级高潮毛片| 亚洲啪AV永久无码精品放毛片| 亚洲黄色中文字幕| 亚洲gv白嫩小受在线观看 | 亚洲国产a级视频| 国产成人精品免费视| 亚洲免费观看视频|