SimpleRpc-序列化與反序列化的設計與實現

haolujun發表於2017-09-21

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


假設你是客戶端,現在要呼叫遠端的加法計算服務,你與服務端商定好了傳送資料的格式:傳送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介面(你甚至都可以自己針對特定的請求響應硬編碼位元組流),給使用者更多的選擇性。

 

相關文章