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

作者: 京東零售 肖夢圓

前序

某一日晚上上線,測試同學在迴歸項目黃金流程時,有一個工單項目接口報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 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;
}



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

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



此去經年,江湖再見

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章