最近在設計一個RPC框架,須要處理序列化的問題。有不少種序列化協議能夠選擇,好比Java原生的序列化協議,Protobuf, Thrift, Hessian, Kryo等等,這裏說的序列化協議專指Java的基於二進制的協議,不是基於XML, JSON這種格式的協議。在實際開發中考慮了不少點,也遇到一些問題,拿出來講說。java
拋開這些協議不說,結合實際的需求,一個理想的序列化協議至少考慮4個方面:算法
3. 是否支持被序列化對象新舊版本的兼容性問題。這個需求在實際開發中常常遇到,好比發佈了一個服務,有不少客戶端使用。當服務須要修改,新 添加1個參數時,不可能要求全部客戶端都更新,那樣牽扯的面太大,因此要作到新舊版本的兼容數組
4. 是否能夠直接序列化對象,而不須要額外的輔助類,好比用IDL生成輔助的序列化類框架
前3個要求是衡量一個序列化協議好壞的重點,第4點是一個使用性的考慮,畢竟在不考慮跨平臺調用的狀況下,不須要使用IDL。使用IDL的開發方式通常是從IDL文件開始的,而不是直接從Java類開始。優化
序列化這件事說白了就是把一個對象變成一個二進制流,而後把二進制流再轉化成對象的過程。前者好說,關鍵是後者,後者其實就是一個如何分幀(Frame)的問題,即從哪一個字節開始讀幾個字節來還原成數據的問題。常見的分幀方式有:this
1. 加結束符,好比http協議spa
2. 定長設計
3. 消息頭+消息,消息頭能夠包含長度,類型信息code
對於Java序列化來講,確定是第三種方式,可是如何設計這個分幀方式又有不少實現。下面說說上述的4個方面具體有哪些考慮和問題。對象
第一是序列化後的字節數大小。最優的序列化後的字節數大小確定是只有數據的二進制流,這樣沒有任何多餘的分幀信息。若是要作到在二進制流裏不加任何分幀信息來反序列化二進制流,有兩個關鍵點:
我把這個雙方約定分幀方式叫作契約。實際操做的時候只須要序列化方按照契約把對象的數據轉成二進制流,反序列化方按照契約把二進制流轉成對象數據。
若是二進制流裏面不加任何的分幀信息,那麼反序列化方只能按照字段的順序來依次分幀。理解一下這句話,若是單純拿到一個只有純數據的二進制流,那麼只能按照約定的順序依次來讀取,而且還得知道每一個字段的長度,這樣才能知道讀取幾個字節來還原數據。在這裏把順序自己做爲一個隱形的契約,雙方按照順序來讀寫。一旦順序錯了,就有可能發生反序列化的錯誤。
第二點,必須有個地方存放這個分幀方式信息,並且雙方都能拿到這個信息。咱們很天然而然想到被序列化對象的Class對象是最天然的選擇,並且它還包含了字段的信息,Class.getDeclaredFields()能夠返回類的全部實例字段。若是getDeclaredFields()方法返回的字段在任意JVM上都是一樣的順序,那麼咱們豈不就是能夠指依靠序列化反序列化雙方拿到被序列化的Class對象,而後利用反射機制拿到字段信息就能夠實現最優的序列化後字節數大小嗎?
可是通過個人調研發現,利用反射技術Class.getDeclared()方法返回的字段數組是沒有排序也沒有特定順序的,好比按照聲明的順序。
/** * Returns an array of {@code Field} objects reflecting all the fields * declared by the class or interface represented by this * {@code Class} object. This includes public, protected, default * (package) access, and private fields, but excludes inherited fields. * <strong><span style="color:#FF0000;">The elements in the array returned are not sorted and are not in any * particular order</span></strong>. This method returns an array of length 0 if the class * or interface declares no fields, or if this {@code Class} object * represents a primitive type, an array class, or void. * * <p> See <em>The Java Language Specification</em>, sections 8.2 and 8.3. * * @return the array of {@code Field} objects representing all the * declared fields of this class * @exception SecurityException * If a security manager, <i>s</i>, is present and any of the * following conditions is met: * * <ul> * * <li> invocation of * {@link SecurityManager#checkMemberAccess * s.checkMemberAccess(this, Member.DECLARED)} denies * access to the declared fields within this class * * <li> the caller's class loader is not the same as or an * ancestor of the class loader for the current class and * invocation of {@link SecurityManager#checkPackageAccess * s.checkPackageAccess()} denies access to the package * of this class * * </ul> * * @since JDK1.1 */ @CallerSensitive public Field[] getDeclaredFields() throws SecurityException { // be very careful not to change the stack depth of this // checkMemberAccess call for security reasons // see java.lang.SecurityManager.checkMemberAccess checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true); return copyFields(privateGetDeclaredFields(false)); }
那不能利用反射技術得到字段順序,能不能利用字節碼技術來得到這個類聲明時存放的字段順序呢?好比用ASM來直接讀Class文件。可是我查閱了Java虛擬機規範,虛擬機規範只規定了Class文件中的元素,並無要求實際存儲的Filed[]按照聲明順序存儲。這也是對的,實際的虛擬機實現能夠按照各自的算法來優化。
事實上目前沒有哪一個協議作到最優的序列化後字節數,間接證實了只使用Class元數據來分幀是不能知足全部平臺的,是不可靠的。
既然順序這種弱契約關係不可靠,那麼須要一種強契約關係,須要把一些分幀信息加入到二進制流,而後經過某種方式來獲取這些分幀信息。加入哪些分幀信息和如何共享這些分幀信息有幾種作法:
1. Java原生的序列化協議把字段類型信息用字符串格式寫到了二進制流裏面,這樣反序列化方就能夠根據字段信息來反序列化。可是Java原生的序列化協議最大的問題就是生成的字節流太大
2. Hessian, Kryo這些協議不須要藉助中間文件,直接把分幀信息寫入了二進制流,而且沒有使用字符串來存放,而是定義了特定的格式來表示這些類型信息。Hessian, Kryo生成的字節流就優化了不少,尤爲是Kryo,生成的字節流大小甚至能夠優於Protobuf.
3. Protobuf和Thrift利用IDL來生成中間文件,這些中間文件包含了如何分幀的信息,好比Thrift給每一個字段生成了元數據,包含了順序信息(加了id信息),和類型信息,實際寫的二進制流裏面包含了每一個字段id, 類型,長度等分幀信息。序列化方和反序列化方共享這些中間文件來進行序列化操做。
Hessian, Kryo, Protobuf, Thrift在生成的字節數都有了優化,而且能夠只發送部分設置了值的字段信息來完成序列化,這樣節省的字節數就更多了。可是還有些問題:
1. Hessian, Kryo不知足第三個方面,支持被序列化對象的新舊版本兼容,只依靠Class信息沒有辦法知道新舊Class的區別
2. Protobuf和Thrift已經很優化了,可是須要用IDL來生成靜態的中間文件。
第二個方面考量序列化和反序列化效率,算法越簡單固然效率就越高。實際的對比來講,Kryo, Protobuf > Thrift > Hessian > Java原生序列化協議
第三方面是個重要考量,好比服務方給方法的參數新增長了一個字段,要能作到老的客戶端還可使用這個新服務。這就要求序列化協議讀取到不能識別的字段後可以處理異常。好比Thrift能夠經過字段的id信息來知道是否支持這個字段,若是不支持讀取,就跳過,從而作到新舊版本的兼容。而Kryo這種不依賴中間文件的協議很難作到這點,由於單純的Class信息在不一樣的平臺下字段順序是不肯定的,而且同一個Java文件在不一樣平臺下編譯後的Class文件中,字段信息也是不肯定的。
第四方面,不依賴中間文件來序列化並同時知足前3點,從上面的分析來看很難作到。Protobuf和Thrift這種使用IDL來生產中間文件的協議,除了從跨平臺調用的角度的須要,也包含了序列化的須要。
目前我尚未看到同時知足4個方面的序列化協議,上面的分析不少是本身的思考,可能有不對的地方,多交流。後面會陸續分析幾種協議的實現。