為什么需要序列化和反序列化?
假設你是客戶端,現在要調用遠程的加法計算服務,你與服務端商定好了發送數據的格式:發送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接口,必須實現三個函數:
- serialize函數,把request/response序列化到參數指定的數組中。
- deserialize函數,把參數指定的數組中的二進制字節流反序列化成request/response。
- 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接口(你甚至都可以自己針對特定的請求響應硬編碼字節流),給用戶更多的選擇性。
文章列表