文章出處

為什么需要序列化和反序列化?


假設你是客戶端,現在要調用遠程的加法計算服務,你與服務端商定好了發送數據的格式:發送8個字節的請求,前4字節是第一個數,后4字節是第二個數,服務端讀取數據的時候也按照商定的方式讀取。其實,這就是一個序列化和反序列化的過程。序列化:2個數字變成8個字節數據,反序列化:8個字節數據變成2個數字。但是這么做有個問題,那就是太容易出錯,每次你還得考慮按照什么形式排列字段,每個字段幾個字節,還要考慮大端小端等。

為了解決這個重復性并且容易出錯的過程,我們有一個小小的改進:把常用數據類型的序列化和反序列化代碼封裝成基礎庫:

int readInt(char *, int size) //讀一個整數
int writeInt(int, char *, int size) //寫一個整數
double readDouble(char *, int size) //讀一個double型數
int writeDouble(double, char *, int size) //寫一個double型數
float readFloat(char *, int size) //讀一個浮點數
int writeFloat(float, char *, int size) //寫一個浮點數
string readString(char *, int size) //讀一個字符串
int writeString(string, char *, int size) //寫一個字符串

現在,我們可以序列化任何基礎類型數據。但是有個問題來了:怎么序列化結構體咧?仔細想一下,結構體也是由最基本的數據類型組成的啊,我們可能會有下面的方案:

class SimpleRequest { 
  int a;
  int b;
   
   int serialize(char *buf, int size) {
    writeInt(a, buf, size);
    writeInt(b, buf + 4, size - 4);
     return 8;
   }

   int deserialize(char *buf, int size) {
    a = readInt(buf, size);
    b = readInt(buf + 4, size - 4);
    return 0;
  }
};

但有些結構體中套用結構體,這種情況怎么處理呢?很好辦,因為只要是結構體我們就已經實現了serialize和deserialize接口,只要調用這兩個函數就可以。所以,最終的方案就是:對于基礎數據類型,通過readXX和writeXX序列化,結構體類型通過serialize/deserialize序列化。

由于基礎數據類型數目有限可枚舉,并且結構體定義也有一定的語法,我們完全可以設計一個語法解析器,讀取IDL定義的文件,自動生成序列化和反序列化的代碼。大致流程如下:使用BNF范式來編寫規則,用來描述我們自己定義的IDL(接口描述語言);然后使用JAVACC或者YACC根據編寫的BNF范式生成解析IDL語言的代碼,利用生成的代碼解析我們用IDL定義的結構體文件,根據語法樹查找其中的基礎數據類型、用戶自定義結構體,并進行有針對性的進行解析。Thrift和grpc的IDL解析都是這么做的,有興趣的同學可以自己玩一下Javacc和yacc。

SimpleRpc的序列化與反序列化設計方案


SimpleRpc沒有自己的序列化和反序列化具體實現方案,它要求用戶自己實現這部分代碼。我們的例子中使用的protobuf,protobuf在SimpleRpc并不是必須的,你可以換成任何一種序列化方式。SimpleRpc的設計方案如下圖所示:

Request和Response是請求和響應的基類,繼承自Serializable接口,必須實現三個函數:

  1. serialize函數,把request/response序列化到參數指定的數組中。
  2. deserialize函數,把參數指定的數組中的二進制字節流反序列化成request/response。
  3. bytes函數,得到結構體序列化成字節流的大小。

AddRequest和AddResponse是用戶端必須實現的代碼,我的例子中在這兩個類里面嵌套了protobuf定義的request和response,當框架根據多態調用序列化和反序列化函數時,相應的類通過調用其成員protobuf實例的序列化和反序列化代碼。由于框架所看到的結構都是Request或者是Response,隱藏其中的protobuf對框架而言是不可見的,你可以更換成任意一種序列化和反序列化方式。

小伙伴們可能有疑問,為什么AddRequest和AddResponse不直接繼承自Serialzable,而是繼承自中間的那層Request和Response,是不是多余了?是因為,Request和Response除了實現序列化和反序列化之外,還有其它接口需要實現,這里面為了只突出序列化相關而忽略了其它接口。

與其它RPC的設計方案對比


最早接觸到的序列化是在Java的遠程調用RMI中,但是Java的序列化太笨拙,它不僅序列化數據成員,還序列化其對象間引用關系,這導致其序列化后的字節數非常多,不是一種高效率的手段。接下來遇到的就是ICE以及Thrift中序列化,但是其序列化模塊是和整個框架綁定到一起,為了只用一個序列化功能,你必須安裝整個框架,還是有些笨拙。直到遇到了protobuf,它真正的把序列化從RPC框架中抽離出來,成為了現在使用最多的序列化框架。

我們的RPC和其它的RPC的不同點就在于,序列化和框架是分離的,你可以自由更換序列化方式,只要你實現了Request和Response接口(你甚至都可以自己針對特定的請求響應硬編碼字節流),給用戶更多的選擇性。

 


文章列表


不含病毒。www.avast.com
全站熱搜
創作者介紹
創作者 大師兄 的頭像
大師兄

IT工程師數位筆記本

大師兄 發表在 痞客邦 留言(0) 人氣()