Jack Jiang

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

          本文由字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)楊晨曦分享,本文有修訂和改動(dòng)。

          1、引言

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

           
           
          技術(shù)交流:

          - 移動(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-4576-1-1.html

          2、系列文章

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

          IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門(mén)到精通,一篇就夠!

          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í)測(cè)!

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

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

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

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

          IM通訊協(xié)議專題學(xué)習(xí)(十):初識(shí) Thrift 序列化協(xié)議》(* 本文)

          另外:如果您還打算系統(tǒng)地學(xué)習(xí)IM開(kāi)發(fā),建議閱讀《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》。

          3、 概述

          Thrift 是 Facebook 開(kāi)源的一個(gè)高性能,輕量級(jí) RPC 服務(wù)框架,是一套全棧式的 RPC 解決方案,包含序列化與服務(wù)通信能力,并支持跨平臺(tái)/跨語(yǔ)言。

          Thrift整體架構(gòu)如圖所示:

          Thrift 軟件棧定義清晰,各層的組件松耦合、可插拔,能夠根據(jù)業(yè)務(wù)場(chǎng)景靈活組合。

          如圖所示:

          Thrift 本身是一個(gè)比較大的話題,本篇文章不會(huì)涉及到Thrift的全部?jī)?nèi)容,只會(huì)涉及到其中的序列化協(xié)議。

          4、 Binary協(xié)議

          4.1消息格式

          這里通過(guò)一個(gè)示例對(duì) Binary 消息格式進(jìn)行直觀的展示。

          IDL 定義如下:

          //接口

          service SupService {

              SearchDepartmentByKeywordResponse SearchDepartmentByKeyword(

                  1: SearchDepartmentByKeywordRequest request)

          }

           

          //請(qǐng)求

          struct SearchDepartmentByKeywordRequest {

              1: optional string Keyword

              2: optional i32 Limit     

              3: optional i32 Offset

          }

           

          //假設(shè)request的payload如下:

          {

              Keyword: "lark",

              Limit: 50,

              Offset: nil,       

          }

          4.2編碼簡(jiǎn)圖

          4.3編碼具體內(nèi)容

          抓包拿到編碼后的字節(jié)流(轉(zhuǎn)成了十進(jìn)制,方便大家看)。

          /* 接口名長(zhǎng)度 */         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

          /* 消息序號(hào) */           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:客戶端消息。調(diào)用遠(yuǎn)程方法,并且期待對(duì)方發(fā)送響應(yīng);
          • 2)OneWay:客戶端消息。調(diào)用遠(yuǎn)程方法,不期待響應(yīng);
          • 3)Reply:服務(wù)端消息。正常響應(yīng);
          • 4)Exception:服務(wù)端消息。異常響應(yīng)。

          msg_seq_id消息序號(hào)):

          • 1)客戶端使用消息序號(hào)來(lái)處理響應(yīng)的失序到達(dá),實(shí)現(xiàn)請(qǐng)求和響應(yīng)的匹配;
          • 2)服務(wù)端不需要檢查該序列號(hào),也不能對(duì)序列號(hào)有任何的邏輯依賴,只需要響應(yīng)的時(shí)候?qū)⑵湓瓨臃祷丶纯伞?/li>

          2)消息體:

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

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

          具體是:

          • 1)field_type:字段類型,包括 String、I64、Struct、Stop 等;
          • 2)fied_id:字段序號(hào),解碼時(shí)通過(guò)序號(hào)確定字段;
          • 3)len:字段長(zhǎng)度,用于變長(zhǎng)類型,如 String;
          • 4)value:字段值。

          字段類型有兩個(gè)作用:

          • 1)Stop 類型用于停止嵌套解析;
          • 2)非 Stop 類型用于 Skip(Skip 操作是跳過(guò)當(dāng)前字段,會(huì)在「常見(jiàn)問(wèn)題 - 兼容性」進(jìn)行講解)。

          4.5數(shù)據(jù)格式

          定長(zhǎng)數(shù)據(jù)類型:

          變長(zhǎng)數(shù)據(jù)類型:

          5、Compact 協(xié)議

          5.1概述

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

          區(qū)別在于整數(shù)類型(包括變長(zhǎng)類型的長(zhǎng)度)采用了先 zigzag 編碼 ,再 varint 壓縮編碼實(shí)現(xiàn),最大化節(jié)省空間開(kāi)銷。

          那么問(wèn)題來(lái)了,varint 和 zigzag 是什么?

          5.2varint 編碼

          解決的問(wèn)題:定長(zhǎng)存儲(chǔ)的整數(shù)類型絕對(duì)值較小時(shí)空間浪費(fèi)大。

          據(jù)統(tǒng)計(jì),RPC 通信時(shí)大部分時(shí)候傳遞的整數(shù)值都很小,如果使用定長(zhǎng)存儲(chǔ)會(huì)很浪費(fèi)。

          舉個(gè) 🌰,對(duì) i32 類型的 7 進(jìn)行編碼,可以說(shuō)前面 3 個(gè)字節(jié)都浪費(fèi)了:

          00000000 00000000 00000000 00000111

           

          解決思路:將整數(shù)類型由定長(zhǎng)存儲(chǔ)轉(zhuǎn)為變長(zhǎng)存儲(chǔ)(能用 1 個(gè)字節(jié)存下就堅(jiān)決不用 2 個(gè)字節(jié))

          原理并不復(fù)雜,就是將整數(shù)按 7bit 分段,每個(gè)字節(jié)的最高位作為標(biāo)識(shí)位,標(biāo)識(shí)后一個(gè)字節(jié)是否屬于該數(shù)據(jù)。1 代表后面的字節(jié)還是屬于當(dāng)前數(shù)據(jù),0 代表這是當(dāng)前數(shù)據(jù)的最后一個(gè)字節(jié)。

          以 i32 類型,數(shù)值 955 為例,可以看出,由原來(lái)的 4 字節(jié)壓縮到了 2 字節(jié):

          binary編碼:       00000000  00000000  00000011  10111011

          切分:        0000  0000000   0000000   0000111   0111011

          compact編碼:                          00000111  10111011

          當(dāng)然,varint 編碼同樣存在缺陷,那就是存儲(chǔ)大數(shù)的時(shí)候,反而會(huì)比 binary 的空間開(kāi)銷更大:本來(lái) 4 個(gè)字節(jié)存下的數(shù)可能需要 5 個(gè)字節(jié),8 個(gè)字節(jié)存下的數(shù)可能需要 10 個(gè)字節(jié)。

          5.3zigzag 編碼

          解決的問(wèn)題:絕對(duì)值較小的負(fù)數(shù)經(jīng)過(guò) varint 編碼后空間開(kāi)銷較大 舉個(gè) 🌰,i32 類型的負(fù)數(shù)(-11)

           

          原碼:         10000000  00000000  00000000  00001011

          反碼:         11111111  11111111  11111111  11110100

          補(bǔ)碼:         11111111  11111111  11111111  11110101

          varint編碼:   00001111  11111111  11111111  11111111  11110101

          顯然,對(duì)于絕對(duì)值較小的負(fù)數(shù),用 varint 編碼以后前導(dǎo) 1 過(guò)多,難以壓縮,空間開(kāi)銷比 binary 編碼還大。

          解決思路:負(fù)數(shù)轉(zhuǎn)正數(shù),從而把前導(dǎo) 1 轉(zhuǎn)成前導(dǎo) 0,便于 varint 壓縮

          算法公式 & 步驟 & 示范:

          //算法公式

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

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

           

          /*

           * 算法步驟:

           * 1. 不分正負(fù):符號(hào)位后置,數(shù)值位前移

           * 2. 對(duì)于負(fù)數(shù):符號(hào)位不變,數(shù)值位取反

           */

           

          //示例

          負(fù)數(shù)(-11)

            補(bǔ)碼:                     11111111  11111111  11111111  11110101

            符號(hào)位后置,數(shù)值位前移:      11111111  11111111  11111111  11101011

            符號(hào)位不變,數(shù)值位取反(21):  00000000  00000000  00000000  00010101

           

          正數(shù)(11)

            補(bǔ)碼:                     00000000  00000000  00000000  00010101

            符號(hào)位后置,數(shù)值位前移(22):  00000000  00000000  00000000  00101010

          奇怪的知識(shí):為什么取名叫 zigzag?

          因?yàn)檫@個(gè)算法將負(fù)數(shù)編碼成正奇數(shù),正數(shù)編碼成偶數(shù)。最后效果是正負(fù)數(shù)穿插向前。

          就像這樣:

          編碼前       編碼后

            0           0

            -1          1

            1           2

            -2          3

            2           4

          6、Json 協(xié)議

          Thrift 不僅支持二進(jìn)制序列化協(xié)議,也支持 Json 這種文本協(xié)議。

          數(shù)據(jù)格式:

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

          "編號(hào)": {

            "類型": "值"

          }

          //示例

          "1": {

            "str": "keyword"

          }

           

           

          /* struct */

          "編號(hào)": {

            "rec": {

              "成員編號(hào)": {

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

              },

              ...

            }

          }

          //示例

          "1": {

            "rec": {

              "1": {

                "i32": 50

              }

            }

          }

           

           

          /* map */

          "編號(hào)": {

            "map": [

              "鍵類型",

              "值類型",

              元素個(gè)數(shù),

                "鍵1",

                "值1",

                ...

                "鍵n",

                "值n"

             ]

          }

          //示例

          "6": {

            "map": [

              "i64",

              "str",

              1,

              666,

              "mapValue"

            ]

          }

           

          /* List */

          "編號(hào)": {

            "set/lst": [

              "值類型",

              元素個(gè)數(shù),

              "ele1",

              "ele2",

              "elen"

            ]

          }

          //示例

          "2": {

            "lst": [

              "str",

              2,

              "lark","keyword"]

          }

          7、修改字段類型導(dǎo)致協(xié)議解析不一致的通信問(wèn)題

          現(xiàn)象:A 服務(wù)訪問(wèn) B 服務(wù),業(yè)務(wù)邏輯短時(shí)間處理完,但整個(gè)請(qǐng)求 15s 超時(shí),必現(xiàn)。

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

          本質(zhì)原因:string 是變長(zhǎng)編碼,i64 是定長(zhǎng)編碼。由于客戶端沒(méi)有升級(jí),所以反序列化的時(shí)候,會(huì)把 signTime 當(dāng)做 string 類型來(lái)解析。而變長(zhǎng)編碼是 T-L-V 模式,所以解析的時(shí)候會(huì)把 signTime 的低位 4 字節(jié)翻譯成 string 的 length。

          signTime 是時(shí)間戳,大整數(shù),比如:1624206147902,轉(zhuǎn)成二進(jìn)制為:

          100000000 00000000 00000001 01111010 00101010 00111011 00000001 00111110

          低位 4 字節(jié)轉(zhuǎn)成十進(jìn)制為:378 。

          也就是要再讀 378 個(gè)字節(jié)作為 SignTime 的值,這已經(jīng)超過(guò)了整個(gè) payload 的大小,最終導(dǎo)致 Socket 讀超時(shí)。

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

          但是錯(cuò)誤的解析可能會(huì)導(dǎo)致各種預(yù)期之外的情況,包括:

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

          8、通信協(xié)議帶來(lái)的常見(jiàn)問(wèn)題

          8.1兼容性

          1)增加字段:

          通過(guò) skip 來(lái)跳過(guò)增加的字段,從而保證兼容性。

          2)刪除字段:

          編譯生成的解析代碼是基于 field_id 的 switch-case 結(jié)構(gòu),語(yǔ)法結(jié)構(gòu)上直接具備兼容性。

          3)修改字段名:

          不破壞兼容性,因?yàn)?binary 協(xié)議不會(huì)對(duì) name 進(jìn)行編碼。

          8.2Exception

          Thrift 有兩種 Exception:

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

          框架內(nèi)置的異常包括:

          • 1)方法名錯(cuò)誤;
          • 2)消息序列號(hào)錯(cuò)誤;
          • 3)協(xié)議錯(cuò)誤。

          這些異常由框架捕獲并封裝成 Exception 消息,反序列化時(shí)會(huì)轉(zhuǎn)成 error 并拋給上層。

          邏輯如下:

          另一種異常是由用戶在 IDL 中自定義的,關(guān)鍵字是 exception,用法上跟 struct 沒(méi)有太大區(qū)別。

          8.3optional、require 實(shí)現(xiàn)原理

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

          字段被標(biāo)識(shí)為 optional 之后:

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

          字段被標(biāo)識(shí)為 require 之后:

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

          9、參考資料

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

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

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

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

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

          [6] 移動(dòng)端IM開(kāi)發(fā)需要面對(duì)的技術(shù)問(wèn)題(含通信協(xié)議選擇)

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

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

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

          [10] 金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)

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


          (本文已同步發(fā)布于:http://www.52im.net/thread-4576-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è)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 民丰县| 达拉特旗| 米易县| 荃湾区| 儋州市| 乌兰浩特市| 离岛区| 胶南市| 从化市| 龙陵县| 湟中县| 同德县| 福泉市| 威宁| 青龙| 吉木萨尔县| 莱州市| 获嘉县| 大连市| 遵化市| 呈贡县| 平阴县| 含山县| 吉隆县| 双桥区| 孙吴县| 华蓥市| 星子县| 乐都县| 伊金霍洛旗| 和静县| 四平市| 东兰县| 博野县| 克山县| 龙川县| 桐梓县| 兴仁县| 喀喇| 娄烦县| 卢龙县|