一次JSF上線問題引發的MsgPack深入理解,保證對你有收穫

京東雲開發者發表於2023-02-09

作者: 京東零售 肖夢圓

前序

某一日晚上上線,測試同學在迴歸專案黃金流程時,有一個工單專案介面報JSF序列化錯誤,馬上升級對應的client包版本,編譯部署後錯誤消失。



線上問題是解決了,但是作為程式設計師要了解問題發生的原因和本質。但這都是為什麼呢?



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

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



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

JDer的開發們都知道JSF介面如果新增欄位需要在類的最後進行新增,對此我檢查了自己的程式碼發現我新增的程式碼也是在類的最後進行新增的,但是特殊之處在於這是一個父類,有子類進行繼承



第三個問題:如果在父類上新增一個欄位有什麼影響呢?

說實話,猛的這麼一問,我猶豫了,JDer們都知道JSF的預設序列化使用的是MsgPack,一直都是口口相傳說如果client類新增欄位必須在類的最後,但是也沒人告訴父類新增欄位咋辦呀,父子類這種場景MsgPack是如何處理序列化和反序列化的?



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

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



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



msgpack介紹

官網地址: https://msgpack.org/

官方介紹:

It's like JSON. but fast and small.

翻譯如下:

這就像JSON,但更快更小

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







JSON格式佔用27位元組,msgpack只佔用18位元組





msgpack 核心壓縮規範

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



format namefirst byte (in binary)first byte (in hex)
positive fixint0xxxxxxx0x00 - 0x7f
fixmap1000xxxx0x80 - 0x8f
fixarray1001xxxx0x90 - 0x9f
fixstr101xxxxx0xa0 - 0xbf
nil110000000xc0
(never used)110000010xc1
false110000100xc2
true110000110xc3
bin 8110001000xc4
bin 16110001010xc5
bin 32110001100xc6
ext 8110001110xc7
ext 16110010000xc8
ext 32110010010xc9
float 32110010100xca
float 64110010110xcb
uint 8110011000xcc
uint 16110011010xcd
uint 32110011100xce
uint 64110011110xcf
int 8110100000xd0
int 16110100010xd1
int 32110100100xd2
int 64110100110xd3
fixext 1110101000xd4
fixext 2110101010xd5
fixext 4110101100xd6
fixext 8110101110xd7
fixext 16110110000xd8
str 8110110010xd9
str 16110110100xda
str 32110110110xdb
array 16110111000xdc
array 32110111010xdd
map 16110111100xde
map 32110111110xdf
negative fixint111xxxxx0xe0 - 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;
}



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

<dependency>
  <groupId>org.msgpack</groupId>
  <artifactId>jackson-dataformat-msgpack</artifactId>
  <version>(version)</version>
</dependency>



測試程式碼如下:

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)之間交換資料。 但是速度更快,體積更小。



此去經年,江湖再見

相關文章