每日觀察!一次JSF上線問題引發的MsgPack深入理解,保證對你有收獲

2023-02-03 12:14:02 來源:51CTO博客

作者: 京東零售 肖夢圓

前序

某一日晚上上線,測試同學在回歸項目黃金流程時,有一個工單項目接口報JSF序列化錯誤,馬上升級對應的client包版本,編譯部署后錯誤消失。

線上問題是解決了,但是作為程序員要了解問題發生的原因和本質。但這都是為什么呢?

第一個問題:為什么測試的時候沒有發現問題呢?

首先預發環境中,所有項目中的JSF別名和client包都是beta,每天都有項目進行編譯部署,這樣每個項目獲取的都是最新的client包,所以在預發環境測試沒有發現


(相關資料圖)

第二個問題:為什么會出現序列化問題?

JDer的開發們都知道JSF接口如果添加字段需要在類的最后進行添加,對此我檢查了自己的代碼發現我添加的代碼也是在類的最后進行添加的,但是特殊之處在于這是一個父類,有子類進行繼承

第三個問題:如果在父類上添加一個字段有什么影響呢?

說實話,猛的這么一問,我猶豫了,JDer們都知道JSF的默認序列化使用的是MsgPack,一直都是口口相傳說如果client類添加字段必須在類的最后,但是也沒人告訴父類添加字段咋辦呀,父子類這種場景MsgPack是如何處理序列化和反序列化的?

第四個問題:MsgPack是什么?MsgPack的序列化和反序化是怎么實現的?

對此問題我坦白了,我不知道;是否有很多JDer跟我對于MsgPack的認識僅限于名字的嗎,更別提是如何實現序列化和反序列化了

到此我已經積累了這么多問題了,是時候努力了解一下MsgPack了,看看什么是MsgPack,為什么JSF的默認序列化選擇MsgPack呢?

msgpack介紹

官網地址:

官方介紹:

It"s like JSON. but fast and small.

翻譯如下:

這就像JSON,但更快更小

MessagePack 是一種高效的二進制序列化格式。它允許您在多種語言(如 JSON)之間交換數據。但是速度更快,體積更小。小整數被編碼成一個字節,而典型的短字符串除了字符串本身之外只需要一個額外的字節。

JSON格式占用27字節,msgpack只占用18字節

msgpack 核心壓縮規范

msgpack制定了??壓縮規范??,這使得msgpack更小更快。我們先了解一下核心規范:

format name

first byte (in binary)

first byte (in hex)

positive fixint

0xxxxxxx

0x00 - 0x7f

fixmap

1000xxxx

0x80 - 0x8f

fixarray

1001xxxx

0x90 - 0x9f

fixstr

101xxxxx

0xa0 - 0xbf

nil

11000000

0xc0

(never used)

11000001

0xc1

false

11000010

0xc2

true

11000011

0xc3

bin 8

11000100

0xc4

bin 16

11000101

0xc5

bin 32

11000110

0xc6

ext 8

11000111

0xc7

ext 16

11001000

0xc8

ext 32

11001001

0xc9

float 32

11001010

0xca

float 64

11001011

0xcb

uint 8

11001100

0xcc

uint 16

11001101

0xcd

uint 32

11001110

0xce

uint 64

11001111

0xcf

int 8

11010000

0xd0

int 16

11010001

0xd1

int 32

11010010

0xd2

int 64

11010011

0xd3

fixext 1

11010100

0xd4

fixext 2

11010101

0xd5

fixext 4

11010110

0xd6

fixext 8

11010111

0xd7

fixext 16

11011000

0xd8

str 8

11011001

0xd9

str 16

11011010

0xda

str 32

11011011

0xdb

array 16

11011100

0xdc

array 32

11011101

0xdd

map 16

11011110

0xde

map 32

11011111

0xdf

negative fixint

111xxxxx

0xe0 - 0xff

示例解讀:

json串:{"compact":true,"schema":0}

對應的msgpack為:82 a7 63 6f 6d 70 61 63 74 c3 a6 73 63 68 65 6d 61 00

第一個82,查看規范表,落在fixmap上,fixmap的范圍:0x80 - 0x8f,表示這是一個map結構,長度為2

后面一個為a7,查看規范表,落在fixstr的范圍:0xa0 - 0xbf,表示是一個字符串,長度為7,后面7個為字符串內容:63 6f 6d 70 61 63 74 將16進制轉化為字符串為:compact

往后一個為:c3,落在true的范圍:oxc3

再往后一個為:a6,查看規范表,落在fixstr的范圍:0xa0 - 0xbf,表示是一個字符串,長度為6,后面6個字符串內容為:

73 63 68 65 6d 61,將16進制轉化為字符串為:schema

最后一個為:00,查看規范表,落在positive fixint,表示一個數字,將16進制轉為10進制數字為:0

拼裝一下{ "compact" : true , "schema" : 0 }

我們看一下官方給出的stringformat示意圖:

對于上面的問題,一個長度大于15(也就是長度無法用4bit表示)的string是這么表示的:用指定字節0xD9表示后面的內容是一個長度用8bit表示的string,比如一個160個字符長度的字符串,它的頭信息就可以表示為D9A0。

舉一個長字符串的例子:

{"name":"fatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfatherfather","age":10,"childerName":"childer"}

83 A4 6E 61 6D 65 DA 03 06 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 66 61 74 68 65 72 A3 61 67 65 0A AB 63 68 69 6C 64 65 72 4E 61 6D 65 A7 63 68 69 6C 64 65 72

一起解析一下看看

83:這個大家都已經知道了,一個固定的map,長度為3

A4:fixstr(長度4),然后找到后面四位

6E 61 6D 65:16進制轉為字符串:name

DA:str 16 ,后面兩個字節為長度

03 06:16進制轉化為10進制:774

后面774個字節轉化為字符串:

A3: fixstr(長度3),然后找到后面三位

61 67 65 :16進制轉為字符串:age

0A :16進制轉10進制:10

AB :fixstr(長度11),然后找到后面11位

63 68 69 6C 64 65 72 4E 61 6D 65 :16進制轉為字符串:childerName

A7 : fixstr(長度7),然后找到后面七位

63 68 69 6C 64 65 72 :16進制轉為字符串:childer

問題原因解析

先還原事件過程,我們在父類的最后添加一個字段,然后創建一個子類繼承父類,然后進行模擬序列化和反序化,查找問題

第一步:模擬父子類,輸出16進制數據

先聲明一個父子類,然后進行序列化

父類:

public class FatherPojo implements Serializable {        /**         * name         */        private String name;}

子類:

public class ChilderPojo  extends FatherPojo implements Serializable {        private String childerName;}

使用官方的序列化包進行序列化

  org.msgpack  jackson-dataformat-msgpack  (version)

測試代碼如下:

public class Demo {        public static void main(String[] args) throws JsonProcessingException {                    ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());                ChilderPojo pojo = new ChilderPojo();                pojo.setName("father");                pojo.setChilderName("childer");                System.out.println(JSON.toJSON(pojo));                byte[] bytes = objectMapper.writeValueAsBytes(pojo);           //輸出16進制             System.out.println(byteToArray(bytes));        }                /**    *  byte數組轉化為16進制數據    */    public static String byteToArray(byte[]data) {                StringBuilder result = new StringBuilder();                for (int i = 0; i < data.length; i++) {                        result.append(Integer.toHexString((data[i] & 0xFF) | 0x100).toUpperCase().substring(1, 3)).append(" ");                }                return result.toString();       }   }

輸入結果如下:

{"name":"father","childerName":"childer"}

82 A4 6E 61 6D 65 A6 66 61 74 68 65 72 AB 63 68 69 6C 64 65 72 4E 61 6D 65 A7 63 68 69 6C 64 65 72

拿著json數據去messagepack官網也獲取一下16進制數據,跟如上代碼輸出的結果是一樣的。

第二步:在父類的結尾增加一個字段,然后輸出16進制數組

修改父類,增加一個age字段

public class FatherPojo implements Serializable {        /**         * name         */        private String name;        /***         * age         */        private Integer age;}

修改測試代碼,給父類的age賦值

public class Demo {        public static void main(String[] args) throws JsonProcessingException {                    ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());                ChilderPojo pojo = new ChilderPojo();                pojo.setName("father");                pojo.setChilderName("childer");          pojo.setAge(10);                      System.out.println(JSON.toJSON(pojo));                byte[] bytes = objectMapper.writeValueAsBytes(pojo);           //輸出16進制             System.out.println(byteToArray(bytes));        }                /**    *  byte數組轉化為16進制數據    */    public static String byteToArray(byte[]data) {                StringBuilder result = new StringBuilder();                for (int i = 0; i < data.length; i++) {                        result.append(Integer.toHexString((data[i] & 0xFF) | 0x100).toUpperCase().substring(1, 3)).append(" ");                }                return result.toString();       }   }

輸入結果如下:

{"name":"father","age":10,"childerName":"childer"}

83 A4 6E 61 6D 65 A6 66 61 74 68 65 72 A3 61 67 65 0A AB 63 68 69 6C 64 65 72 4E 61 6D 65 A7 63 68 69 6C 64 65 72

拿著json數據去messagepack官網也獲取一下16進制數據,跟如上代碼輸出的結果是一樣的。

先對比json數據

父類沒加字段之前:{"name":"father","childerName":"childer"}

父類加字段之后: {"name":"father","age":10,"childerName":"childer"}

對比一下前后兩次16進制數組,我們進行對齊后進行對比一下

82 A4 6E 61 6D 65 A6 66 61 74 68 65 72 AB 63 68 69 6C 64 65 72 4E 61 6D 65 A7 63 68 69 6C 64 65 72

83 A4 6E 61 6D 65 A6 66 61 74 68 65 72 A3 61 67 65 0A AB 63 68 69 6C 64 65 72 4E 61 6D 65 A7 63 68 69 6C 64 65 72

對比發現在紅色部分是多出來的一部分數據應該就是我們添加的age字段,現在我們進行解析對比一下。

拼裝一下{ "name": "father", "childerName" : "childer" }

拼裝一下{ "name": "father", “age”: 10 "childerName" : "childer" }

第三步:直接對二進制數據解包

1、先用正確的順序解包

public static void analyze(byte[] bytes) throws IOException {        MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes);        int length =  unpacker.unpackMapHeader();        String name = unpacker.unpackString();        String nameValue = unpacker.unpackString();        String age = unpacker.unpackString();        Integer ageValue = unpacker.unpackInt();        String childerName = unpacker.unpackString();        String childerNameValue = unpacker.unpackString();        System.out.println("{""+name+"":""+nameValue+"",""+age+"":"+ageValue+",""+childerName+"":""+childerNameValue+""}");}

輸出結果為:

{"name":"father","age":10,"childerName":"childer"}

2、如果我們客戶端沒有升級client包版本,使用了錯誤的解包順序

public static void analyze(byte[] bytes) throws IOException {            MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes);            int length =  unpacker.unpackMapHeader();            String name = unpacker.unpackString();            String nameValue = unpacker.unpackString();    String childerName = unpacker.unpackString();            String childerNameValue = unpacker.unpackString();           System.out.println("{""+name+"":""+nameValue+"",""+childerName+"":""+childerNameValue+""}");    }

解析報錯:反序列化失敗

從上述案例中發現在父類中增加數據,相當于在子類中間增加數據導致子嘞反序列化失敗。需要注意的是解包順序必須與打包順序一致,否則會出錯。也就是說協議格式的維護要靠兩端手寫代碼進行保證,而這是很不安全的。

JSF為什么選擇MsgPack以及官方FAQ解釋

為什么JSF會選擇MsgPack作為默認的序列化

JDer的開發們用的RPC基本上都是JSF,在遠程調用的過程中字節越少傳輸越快越安全(產生丟包的可能性更小), 咱們回過頭去看看MsgPack; 我們了解了MsgPack的壓縮傳輸可以看到,MsgPack序列化后占用的字節更小,這樣傳輸的更快更安全;所以這應該就是JSF選擇Msgpack作為默認序列化的原因了。我理解MsgPack是采用一種空間換時間的策略,減少了在網絡傳輸中的字節數,使其更安全,然后在接到序列化后的數據后按照壓縮規范進行反序列化(這部分增加了cpu和內存的使用,但是減少了網絡傳輸中時間且提高了傳輸安全性)。

JSF對父子類序列化的FQA解釋

是時候進行總結和說再見了

總結:

1、MessagePack 是一種高效的二進制序列化格式。 它允許您在多種語言(如 JSON)之間交換數據。 但是速度更快,體積更小。

此去經年,江湖再見

標簽: 反序列化 這是一個 增加一個

上一篇:
下一篇: